更新go结构和uniapp
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<view class="auth-page">
|
||||
<view class="page-inner">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<FaIcon name="chevron-left" color="#303133" :size="18" />
|
||||
<text class="nav-text">返回</text>
|
||||
</view>
|
||||
|
||||
<view class="header">
|
||||
<text class="title">忘记密码</text>
|
||||
<text class="subtitle">
|
||||
{{ currentStep === 1 ? '验证您的账号' : currentStep === 2 ? '通过手机号验证身份' : '设置新密码' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<!-- 第一步:验证租户和账号 -->
|
||||
<view v-if="currentStep === 1" class="form">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.tenant_name"
|
||||
placeholder="租户名称"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.account"
|
||||
placeholder="账号"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleVerifyAccount">
|
||||
{{ loading ? '验证中...' : '下一步' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二步:验证手机号并发送验证码 -->
|
||||
<view v-if="currentStep === 2" class="form">
|
||||
<view class="info-box">
|
||||
<text class="info-label">租户</text>
|
||||
<text class="info-value">{{ form.tenant_name }}</text>
|
||||
</view>
|
||||
<view class="info-box">
|
||||
<text class="info-label">账号</text>
|
||||
<text class="info-value">{{ form.account }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="verifyResult.phone" class="field">
|
||||
<u-input
|
||||
v-model="form.phone"
|
||||
placeholder="手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="empty-contact">
|
||||
<text>账号未绑定手机号,请联系管理员</text>
|
||||
</view>
|
||||
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="form.sms_code"
|
||||
placeholder="短信验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || codeLoading || !verifyResult.phone }"
|
||||
@tap="handleSendCode"
|
||||
>{{ countdown > 0 ? `${countdown}s` : (codeLoading ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleVerifyPhone">
|
||||
{{ loading ? '验证中...' : '下一步' }}
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<text class="link-text" @tap="currentStep = 1">返回上一步</text>
|
||||
<text class="link-text" @tap="goBack">返回登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第三步:重置密码 -->
|
||||
<view v-if="currentStep === 3" class="form">
|
||||
<view class="info-box">
|
||||
<text class="info-label">租户</text>
|
||||
<text class="info-value">{{ form.tenant_name }}</text>
|
||||
</view>
|
||||
<view class="info-box">
|
||||
<text class="info-label">账号</text>
|
||||
<text class="info-value">{{ form.account }}</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.new_password"
|
||||
placeholder="新密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.confirm_password"
|
||||
placeholder="确认新密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleResetPassword">
|
||||
{{ loading ? '提交中...' : '重置密码' }}
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<text class="link-text" @tap="currentStep = 2">返回上一步</text>
|
||||
<text class="link-text" @tap="goBack">返回登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onUnmounted } from 'vue'
|
||||
import { verifyAccount, resetPassword, sendResetCode } from '@/api/auth.js'
|
||||
import { setTenantName, getTenantName } from '@/utils/auth.js'
|
||||
|
||||
const currentStep = ref(1)
|
||||
const loading = ref(false)
|
||||
const codeLoading = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
const form = reactive({
|
||||
tenant_name: getTenantName() || '',
|
||||
account: '',
|
||||
phone: '',
|
||||
sms_code: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
const verifyResult = reactive({
|
||||
phone: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const inputStyle = {
|
||||
backgroundColor: 'transparent',
|
||||
padding: '0 8rpx',
|
||||
height: '96rpx',
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 第一步:验证租户和账号
|
||||
async function handleVerifyAccount() {
|
||||
if (loading.value) return
|
||||
if (!form.tenant_name.trim() || !form.account.trim()) {
|
||||
uni.showToast({ title: '请填写租户和账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await verifyAccount({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim()
|
||||
})
|
||||
verifyResult.phone = data?.phone || ''
|
||||
verifyResult.email = data?.email || ''
|
||||
if (!verifyResult.phone && !verifyResult.email) {
|
||||
uni.showToast({ title: '账号未绑定验证方式', icon: 'none' })
|
||||
return
|
||||
}
|
||||
form.phone = verifyResult.phone || ''
|
||||
currentStep.value = 2
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:发送验证码
|
||||
async function handleSendCode() {
|
||||
if (codeLoading.value || countdown.value > 0) return
|
||||
if (!form.phone.trim()) {
|
||||
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(form.phone)) {
|
||||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
codeLoading.value = true
|
||||
try {
|
||||
await sendResetCode({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
phone: form.phone.trim(),
|
||||
channel: 'sms'
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
codeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步验证:验证手机和验证码
|
||||
async function handleVerifyPhone() {
|
||||
if (loading.value) return
|
||||
if (!form.phone.trim()) {
|
||||
uni.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.sms_code.trim()) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 后端的验证码验证在重置密码时进行,这里只是提交到第三步
|
||||
currentStep.value = 3
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 第三步:重置密码
|
||||
async function handleResetPassword() {
|
||||
if (loading.value) return
|
||||
if (!form.tenant_name.trim() || !form.account.trim() || !form.phone.trim()) {
|
||||
uni.showToast({ title: '请填写租户、账号和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.new_password) {
|
||||
uni.showToast({ title: '请输入新密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.new_password !== form.confirm_password) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.new_password.length < 6) {
|
||||
uni.showToast({ title: '密码长度不能少于6个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.sms_code.trim()) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await resetPassword({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
phone: form.phone.trim(),
|
||||
sms_code: form.sms_code.trim(),
|
||||
new_password: form.new_password,
|
||||
confirm_password: form.confirm_password
|
||||
})
|
||||
setTenantName(form.tenant_name.trim())
|
||||
uni.showToast({ title: '重置成功,请登录', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
// error handled by request interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.page-inner {
|
||||
padding: 0 48rpx;
|
||||
padding-top: calc(var(--status-bar-height, 44px) + 24rpx);
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 0 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-link {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding-left: 16rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: $color-text;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-contact {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $color-primary;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-top: 12rpx;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding: 16rpx 0 4rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<web-view :src="webviewSrc" @message="onMessage" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
const webviewSrc = ref('')
|
||||
let eventChannel = null
|
||||
|
||||
onLoad((query) => {
|
||||
const captchaId = decodeURIComponent(query.captchaId || '')
|
||||
webviewSrc.value = `/static/html/geetest-captcha.html?captchaId=${encodeURIComponent(captchaId)}`
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1]
|
||||
eventChannel = page.getOpenerEventChannel?.()
|
||||
})
|
||||
|
||||
function onMessage(e) {
|
||||
const payload = (e.detail && e.detail.data && e.detail.data[0]) || {}
|
||||
if (payload.type === 'success') {
|
||||
eventChannel?.emit('geetestSuccess', payload.result || {})
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
eventChannel?.emit('geetestFail', payload.msg || '人机验证未通过')
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
+379
-215
@@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="page-inner">
|
||||
<!-- 品牌区 -->
|
||||
<view class="header">
|
||||
<view class="logo">
|
||||
<text class="logo-char">云</text>
|
||||
</view>
|
||||
<text class="title">欢迎回来</text>
|
||||
<text class="subtitle">登录云泽,开启你的旅程</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录方式切换 -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab"
|
||||
@@ -24,75 +19,117 @@
|
||||
>账号登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单 -->
|
||||
<view class="form-card">
|
||||
<view class="form">
|
||||
<template v-if="loginType === 'phone'">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="phone"
|
||||
placeholder="请输入手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
>
|
||||
<template #prefix>
|
||||
<text class="prefix">+86</text>
|
||||
</template>
|
||||
</u-input>
|
||||
</view>
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="code"
|
||||
placeholder="请输入验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
v-model="tenantName"
|
||||
placeholder="请输入租户名称"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 }"
|
||||
@tap="sendCode"
|
||||
>{{ countdown > 0 ? `${countdown}s 后重发` : '获取验证码' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="username"
|
||||
placeholder="请输入账号"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="password"
|
||||
placeholder="请输入密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="loginType === 'phone'">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="phone"
|
||||
placeholder="请输入手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
>
|
||||
<template #prefix>
|
||||
<text class="prefix">+86</text>
|
||||
</template>
|
||||
</u-input>
|
||||
</view>
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="smsCode"
|
||||
placeholder="请输入验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || sendingCode }"
|
||||
@tap="sendPhoneCode"
|
||||
>{{ countdown > 0 ? `${countdown}s 后重发` : (sendingCode ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="btn-primary" @tap="handleLogin">登 录</view>
|
||||
<view class="btn-ghost" @tap="handleTestLogin">测试登录</view>
|
||||
<template v-else>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="username"
|
||||
placeholder="请输入账号"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="password"
|
||||
placeholder="请输入密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<!-- 后台开启短信/邮箱登录校验时展示 -->
|
||||
<view v-if="needLoginCode" class="field field-row">
|
||||
<u-input
|
||||
v-model="verifyCode"
|
||||
placeholder="请输入登录验证码"
|
||||
type="number"
|
||||
maxlength="8"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || sendingCode }"
|
||||
@tap="sendAccountVerifyCode"
|
||||
>{{ countdown > 0 ? `${countdown}s 后重发` : (sendingCode ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
<view v-if="verifyHint" class="verify-hint">
|
||||
<text>{{ verifyHint }}</text>
|
||||
</view>
|
||||
<view class="remember-row" @tap="rememberMe = !rememberMe">
|
||||
<view class="remember-dot" :class="{ on: rememberMe }">
|
||||
<text v-if="rememberMe" class="remember-check">✓</text>
|
||||
</view>
|
||||
<text class="remember-text">记住我</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: submitting }" @tap="handleLogin">
|
||||
{{ submitting ? '登录中...' : '登 录' }}
|
||||
</view>
|
||||
|
||||
<view class="link-row">
|
||||
<text class="link" @tap="goRegister">注册账号</text>
|
||||
<text class="link" @tap="goForget">忘记密码</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议 -->
|
||||
<view class="agreement" @tap="agreed = !agreed">
|
||||
<view class="agree-dot" :class="{ on: agreed }">
|
||||
<text v-if="agreed" class="agree-check">✓</text>
|
||||
@@ -104,37 +141,43 @@
|
||||
<text class="agree-link">《隐私政策》</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 第三方 -->
|
||||
<view class="oauth">
|
||||
<text class="oauth-label">其他方式</text>
|
||||
<view class="oauth-icons">
|
||||
<view class="oauth-btn" @tap="socialLogin('wechat')">
|
||||
<FaIcon name="weixin" type="brands" color="#3c9cff" :size="22" />
|
||||
</view>
|
||||
<view class="oauth-btn" @tap="socialLogin('qq')">
|
||||
<FaIcon name="qq" type="brands" color="#3c9cff" :size="22" />
|
||||
</view>
|
||||
<view class="oauth-btn" @tap="socialLogin('alipay')">
|
||||
<FaIcon name="alipay" type="brands" color="#3c9cff" :size="22" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { loginSuccess, isLoggedIn } from '@/utils/auth.js'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import {
|
||||
login,
|
||||
loginBySms,
|
||||
sendLoginCode,
|
||||
fetchVerifyConfig
|
||||
} from '@/api/auth.js'
|
||||
import {
|
||||
loginSuccess,
|
||||
isLoggedIn,
|
||||
setTenantName,
|
||||
getTenantName,
|
||||
getRememberLogin,
|
||||
saveRememberLogin,
|
||||
clearRememberLogin
|
||||
} from '@/utils/auth.js'
|
||||
import { showGeetest4 } from '@/utils/geetest.js'
|
||||
|
||||
const loginType = ref('phone')
|
||||
const loginType = ref('account')
|
||||
const tenantName = ref('')
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const smsCode = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const verifyCode = ref('')
|
||||
const agreed = ref(true)
|
||||
const rememberMe = ref(false)
|
||||
const countdown = ref(0)
|
||||
const sendingCode = ref(false)
|
||||
const submitting = ref(false)
|
||||
const openVerify = ref(false)
|
||||
const verifyType = ref('')
|
||||
let timer = null
|
||||
|
||||
const inputStyle = {
|
||||
@@ -143,81 +186,235 @@ const inputStyle = {
|
||||
height: '96rpx',
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
onMounted(() => {
|
||||
if (isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
}
|
||||
const needLoginCode = computed(() => {
|
||||
return openVerify.value && (verifyType.value === 'sms' || verifyType.value === 'email')
|
||||
})
|
||||
|
||||
function sendCode() {
|
||||
if (countdown.value > 0) return
|
||||
const needGeetest = computed(() => {
|
||||
return loginType.value === 'account' && !needLoginCode.value
|
||||
})
|
||||
|
||||
const verifyHint = computed(() => {
|
||||
if (!openVerify.value) return ''
|
||||
if (verifyType.value === 'sms') return '已开启短信验证,请先获取验证码'
|
||||
if (verifyType.value === 'email') return '已开启邮箱验证,请先获取验证码'
|
||||
return ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
return
|
||||
}
|
||||
const remembered = getRememberLogin()
|
||||
if (remembered.rememberMe) {
|
||||
rememberMe.value = true
|
||||
tenantName.value = remembered.tenantName
|
||||
username.value = remembered.account
|
||||
password.value = remembered.password
|
||||
} else {
|
||||
tenantName.value = getTenantName()
|
||||
}
|
||||
const cfg = await fetchVerifyConfig()
|
||||
openVerify.value = cfg.openVerify
|
||||
verifyType.value = cfg.verifyType
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function sendPhoneCode() {
|
||||
if (countdown.value > 0 || sendingCode.value) return
|
||||
if (!tenantName.value.trim()) {
|
||||
uni.showToast({ title: '请输入租户名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) clearInterval(timer)
|
||||
}, 1000)
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
sendingCode.value = true
|
||||
try {
|
||||
await sendLoginCode({
|
||||
tenant_name: tenantName.value.trim(),
|
||||
account: phone.value.trim(),
|
||||
channel: 'sms'
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch {
|
||||
// toast 已在 request 中处理
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function sendAccountVerifyCode() {
|
||||
if (countdown.value > 0 || sendingCode.value) return
|
||||
if (!tenantName.value.trim()) {
|
||||
uni.showToast({ title: '请输入租户名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const channel = verifyType.value === 'email' ? 'email' : 'sms'
|
||||
sendingCode.value = true
|
||||
try {
|
||||
await sendLoginCode({
|
||||
tenant_name: tenantName.value.trim(),
|
||||
account: username.value.trim(),
|
||||
channel
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch {
|
||||
// handled
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function navigateAfterLogin() {
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
}, 500)
|
||||
}, 400)
|
||||
}
|
||||
|
||||
function handleTestLogin() {
|
||||
loginSuccess({ nickname: '测试用户', phone: '13800000000' })
|
||||
navigateAfterLogin()
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
async function submitAccountLogin(geetestResult = null) {
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (loginType.value === 'phone') {
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code.value || code.value.length < 4) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loginSuccess({ phone: phone.value, nickname: '用户' + phone.value.slice(-4) })
|
||||
if (!password.value.trim()) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (needLoginCode.value && !verifyCode.value.trim()) {
|
||||
uni.showToast({ title: '请输入登录验证码', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
const payload = {
|
||||
tenant_name: tenantName.value.trim(),
|
||||
account: username.value.trim(),
|
||||
password: password.value
|
||||
}
|
||||
if (needLoginCode.value) {
|
||||
payload.code = verifyCode.value.trim()
|
||||
}
|
||||
if (geetestResult) {
|
||||
Object.assign(payload, geetestResult)
|
||||
}
|
||||
|
||||
const data = await login(payload)
|
||||
setTenantName(tenantName.value.trim())
|
||||
loginSuccess({
|
||||
token: data.token,
|
||||
user: data.user
|
||||
})
|
||||
if (rememberMe.value) {
|
||||
saveRememberLogin({
|
||||
tenantName: tenantName.value.trim(),
|
||||
account: username.value.trim(),
|
||||
password: password.value
|
||||
})
|
||||
} else {
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!password.value.trim()) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loginSuccess({ nickname: username.value })
|
||||
clearRememberLogin()
|
||||
}
|
||||
navigateAfterLogin()
|
||||
return true
|
||||
}
|
||||
|
||||
function socialLogin(type) {
|
||||
async function handleLogin() {
|
||||
if (submitting.value) return
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const names = { wechat: '微信', qq: 'QQ', alipay: '支付宝' }
|
||||
loginSuccess({ nickname: names[type] + '用户' })
|
||||
uni.showToast({ title: `${names[type]}登录成功`, icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/dashboard/dashboard' })
|
||||
}, 500)
|
||||
if (!tenantName.value.trim()) {
|
||||
uni.showToast({ title: '请输入租户名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (loginType.value === 'phone') {
|
||||
if (!/^1\d{10}$/.test(phone.value)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!smsCode.value || smsCode.value.length < 4) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const data = await loginBySms({
|
||||
tenant_name: tenantName.value.trim(),
|
||||
phone: phone.value.trim(),
|
||||
code: smsCode.value.trim()
|
||||
})
|
||||
setTenantName(tenantName.value.trim())
|
||||
loginSuccess({
|
||||
token: data.token,
|
||||
user: data.user || { phone: phone.value, nickname: '用户' + phone.value.slice(-4) }
|
||||
})
|
||||
navigateAfterLogin()
|
||||
return
|
||||
}
|
||||
|
||||
if (needGeetest.value) {
|
||||
if (!username.value.trim()) {
|
||||
uni.showToast({ title: '请输入账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!password.value.trim()) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const geetestResult = await showGeetest4()
|
||||
const ok = await submitAccountLogin(geetestResult)
|
||||
if (ok) navigateAfterLogin()
|
||||
} catch (err) {
|
||||
const msg = err?.message || '人机验证失败'
|
||||
if (msg !== '人机验证未通过') {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await submitAccountLogin()
|
||||
if (ok) navigateAfterLogin()
|
||||
} catch {
|
||||
// toast 已处理
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goRegister() {
|
||||
uni.navigateTo({ url: '/pages/login/register' })
|
||||
}
|
||||
|
||||
function goForget() {
|
||||
uni.navigateTo({ url: '/pages/login/forget' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -238,23 +435,6 @@ function socialLogin(type) {
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: $radius-lg;
|
||||
background: $color-primary-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.logo-char {
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
@@ -296,9 +476,6 @@ function socialLogin(type) {
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
@@ -339,6 +516,47 @@ function socialLogin(type) {
|
||||
}
|
||||
}
|
||||
|
||||
.verify-hint {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
line-height: 1.5;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.remember-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.remember-dot {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 1rpx solid $color-border;
|
||||
background: $color-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.on {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.remember-check {
|
||||
font-size: 18rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $color-primary;
|
||||
@@ -354,22 +572,21 @@ function socialLogin(type) {
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
height: 96rpx;
|
||||
background: $color-card;
|
||||
border: 1rpx solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
.link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 4rpx 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
.link {
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
@@ -377,6 +594,7 @@ function socialLogin(type) {
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
margin-top: 32rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.agree-dot {
|
||||
@@ -411,58 +629,4 @@ function socialLogin(type) {
|
||||
.agree-link {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.oauth {
|
||||
margin-top: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.oauth-label {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
position: relative;
|
||||
padding: 0 32rpx;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 80rpx;
|
||||
height: 1rpx;
|
||||
background: $color-border;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-icons {
|
||||
display: flex;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.oauth-btn {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: $color-card;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<view class="auth-page">
|
||||
<view class="page-inner">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<FaIcon name="chevron-left" color="#303133" :size="18" />
|
||||
<text class="nav-text">返回</text>
|
||||
</view>
|
||||
|
||||
<view class="header">
|
||||
<text class="title">注册账号</text>
|
||||
<text class="subtitle">按租户创建管理员账号</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form">
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.tenant_name"
|
||||
placeholder="租户名称"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.account"
|
||||
placeholder="账号"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.name"
|
||||
placeholder="姓名"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.phone"
|
||||
placeholder="手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field field-row">
|
||||
<u-input
|
||||
v-model="form.sms_code"
|
||||
placeholder="短信验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
<text
|
||||
class="code-link"
|
||||
:class="{ disabled: countdown > 0 || codeLoading }"
|
||||
@tap="handleSendCode"
|
||||
>{{ countdown > 0 ? `${countdown}s` : (codeLoading ? '发送中' : '获取验证码') }}</text>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.email"
|
||||
placeholder="邮箱(可选)"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.password"
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<u-input
|
||||
v-model="form.confirm_password"
|
||||
placeholder="确认密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="btn-primary" :class="{ disabled: loading }" @tap="handleSubmit">
|
||||
{{ loading ? '提交中...' : '注 册' }}
|
||||
</view>
|
||||
<view class="footer-link" @tap="goBack">
|
||||
<text>已有账号?去登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onUnmounted } from 'vue'
|
||||
import { register, sendRegisterCode } from '@/api/auth.js'
|
||||
import { setTenantName } from '@/utils/auth.js'
|
||||
|
||||
const loading = ref(false)
|
||||
const codeLoading = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
const form = reactive({
|
||||
tenant_name: '',
|
||||
account: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
sms_code: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
const inputStyle = {
|
||||
backgroundColor: 'transparent',
|
||||
padding: '0 8rpx',
|
||||
height: '96rpx',
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleSendCode() {
|
||||
if (codeLoading.value || countdown.value > 0) return
|
||||
if (!form.tenant_name || !form.account || !form.phone) {
|
||||
uni.showToast({ title: '请先填写租户、账号和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1\d{10}$/.test(form.phone)) {
|
||||
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
codeLoading.value = true
|
||||
try {
|
||||
await sendRegisterCode({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
phone: form.phone.trim()
|
||||
})
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
startCountdown()
|
||||
} catch {
|
||||
// handled
|
||||
} finally {
|
||||
codeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading.value) return
|
||||
if (!form.tenant_name.trim() || !form.account.trim() || !form.phone.trim()) {
|
||||
uni.showToast({ title: '请填写租户、账号和手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.password) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.password !== form.confirm_password) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
await register({
|
||||
tenant_name: form.tenant_name.trim(),
|
||||
account: form.account.trim(),
|
||||
name: form.name.trim(),
|
||||
phone: form.phone.trim(),
|
||||
sms_code: form.sms_code.trim(),
|
||||
email: form.email.trim(),
|
||||
password: form.password,
|
||||
confirm_password: form.confirm_password
|
||||
})
|
||||
setTenantName(form.tenant_name.trim())
|
||||
uni.showToast({ title: '注册成功,请登录', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
|
||||
}, 500)
|
||||
} catch {
|
||||
// handled
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: '/pages/login/login' }) })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.page-inner {
|
||||
padding: 0 48rpx;
|
||||
padding-top: calc(var(--status-bar-height, 44px) + 24rpx);
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 0 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-link {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding-left: 16rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $color-primary;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-top: 12rpx;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
padding: 16rpx 0 4rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -6,8 +6,8 @@
|
||||
<text class="avatar-text">{{ avatarText }}</text>
|
||||
</view>
|
||||
<view class="profile-detail">
|
||||
<text class="profile-name">{{ user?.nickname || '云泽用户' }}</text>
|
||||
<text class="profile-id">ID: {{ userId }}</text>
|
||||
<text class="profile-name">{{ user?.nickname || user?.name || '云泽用户' }}</text>
|
||||
<text class="profile-id">{{ user?.account ? `账号: ${user.account}` : `ID: ${userId}` }}</text>
|
||||
</view>
|
||||
<view class="profile-edit" @tap="onEdit">
|
||||
<FaIcon name="pen-to-square" color="#ffffff" :size="16" />
|
||||
@@ -43,7 +43,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" @tap="handleLogout">
|
||||
<view class="logout-btn" hover-class="logout-btn-hover" @tap.stop="handleLogout">
|
||||
<text class="logout-text">退出登录</text>
|
||||
</view>
|
||||
|
||||
@@ -56,20 +56,24 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUser, logout, isLoggedIn } from '@/utils/auth.js'
|
||||
import { getUser, setUser, logout, isLoggedIn } from '@/utils/auth.js'
|
||||
import { getCurrentUser, logoutApi } from '@/api/auth.js'
|
||||
import AppTabbar from '@/components/AppTabbar.vue'
|
||||
|
||||
const user = ref(null)
|
||||
|
||||
const avatarText = computed(() => {
|
||||
const name = user.value?.nickname || '云'
|
||||
const name = user.value?.nickname || user.value?.name || '云'
|
||||
return name.charAt(0).toUpperCase()
|
||||
})
|
||||
|
||||
const userId = computed(() => {
|
||||
return user.value?.phone
|
||||
? user.value.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
: '10086'
|
||||
if (user.value?.phone) {
|
||||
return user.value.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
if (user.value?.account) return user.value.account
|
||||
if (user.value?.id) return String(user.value.id)
|
||||
return '-'
|
||||
})
|
||||
|
||||
const profileStats = ref([
|
||||
@@ -96,12 +100,32 @@ const menuGroups = ref([
|
||||
]
|
||||
])
|
||||
|
||||
async function loadUser() {
|
||||
user.value = getUser()
|
||||
try {
|
||||
const data = await getCurrentUser()
|
||||
if (data) {
|
||||
const prev = getUser() || {}
|
||||
const merged = {
|
||||
...prev,
|
||||
...data,
|
||||
nickname: data.name || data.nickname || data.account || prev.nickname || '云泽用户'
|
||||
}
|
||||
setUser(merged)
|
||||
user.value = merged
|
||||
}
|
||||
} catch {
|
||||
// 401 时 request 会跳转登录
|
||||
user.value = getUser()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
user.value = getUser()
|
||||
loadUser()
|
||||
})
|
||||
|
||||
function onEdit() {
|
||||
@@ -109,6 +133,10 @@ function onEdit() {
|
||||
}
|
||||
|
||||
function onMenuTap(item) {
|
||||
if (item.title === '账号安全') {
|
||||
uni.navigateTo({ url: '/pages/login/forget' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: item.title, icon: 'none' })
|
||||
}
|
||||
|
||||
@@ -116,11 +144,15 @@ function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
confirmText: '退出',
|
||||
cancelText: '取消',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
logout()
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
if (!res.confirm) return
|
||||
// 先清本地并跳转,避免接口超时/失败导致“退出无效”
|
||||
logout()
|
||||
// 后端无状态退出,失败可忽略(不阻塞)
|
||||
logoutApi().catch(() => {})
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -289,18 +321,20 @@ function handleLogout() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
.logout-btn-hover {
|
||||
background: $color-bg-page !important;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-secondary;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 24rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user