更新go结构和uniapp

This commit is contained in:
2026-07-15 22:38:33 +08:00
parent bca5170e22
commit 61a937f7a3
23 changed files with 3536 additions and 293 deletions
+379 -215
View File
@@ -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>