feat: 完成全流程融资助手应用开发

- 新增文章、课程详情页面,完善融资课堂模块
- 新增登录/注册、评估填报、结果匹配全流程业务
- 新增会话管理、错误重试与状态加载逻辑
- 重构首页路由逻辑,适配已登录用户跳转
- 完善个人中心登出与数据持久化功能
This commit is contained in:
hujun
2026-09-11 20:23:10 +08:00
parent 60b58442ce
commit 626cef9d38
14 changed files with 3016 additions and 76 deletions
+86 -7
View File
@@ -5,12 +5,14 @@
<BrandLogo />
<text class="welcome">欢迎登录 AI融资</text>
<view class="login-tabs">
<view :class="mode == 'phone' ? 'login-tab selected' : 'login-tab'" @click="mode = 'phone'"><text>手机号登录</text></view>
<view :class="mode == 'password' ? 'login-tab selected' : 'login-tab'" @click="mode = 'password'"><text>密码登录</text></view>
<view :class="mode == 'phone' ? 'login-tab selected' : 'login-tab'" @click="switchMode('phone')"><text>手机号登录</text></view>
<view :class="mode == 'password' ? 'login-tab selected' : 'login-tab'" @click="switchMode('password')"><text>密码登录</text></view>
</view>
<view class="input-box"><text class="input-prefix">+86</text><input v-model="phone" type="number" maxlength="11" placeholder="请输入手机号" /></view>
<view v-if="mode == 'password'" class="input-box"><text class="input-prefix">密码</text><input v-model="password" password placeholder="请输入登录密码" /></view>
<button class="login-button" @click="login">{{ mode == 'phone' ? '获取验证码' : '登录' }}</button>
<view v-if="mode == 'phone' && smsSent" class="input-box"><text class="input-prefix">验证码</text><input v-model="smsCode" type="number" maxlength="6" placeholder="请输入 6 位验证码" /><text class="countdown" @click="resendSms">{{ countdown > 0 ? countdown.toString() + 's' : '重新获取' }}</text></view>
<text v-if="errorMessage.length > 0" class="error-message">{{ errorMessage }}</text>
<button class="login-button" :disabled="loading" @click="login">{{ loading ? '处理中...' : (mode == 'phone' ? (smsSent ? '登录' : '获取验证码') : '登录') }}</button>
<view class="other-login"><view class="line"></view><text>其他登录方式</text><view class="line"></view></view>
<view class="social-row">
<view class="social wechat"><AppIcon name="message" :size="38" /></view><view class="social apple"><AppIcon name="lock" :size="38" /></view><view class="social phone"><AppIcon name="user" :size="38" /></view>
@@ -23,16 +25,90 @@
<script setup lang="uts">
import BrandLogo from '@/components/BrandLogo.uvue'
import AppIcon from '@/components/AppIcon.uvue'
import { requestSmsCode } from '@/services/api.uts'
import { requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/services/api.uts'
const mode = ref('phone')
const phone = ref('13800138000')
const password = ref('')
const smsCode = ref('')
const smsSent = ref(false)
const countdown = ref(0)
const loading = ref(false)
const errorMessage = ref('')
const agreed = ref(true)
const login = async () => {
if (!agreed.value) { uni.showToast({ title: '请先同意用户协议', icon: 'none' }); return }
if (mode.value == 'phone') await requestSmsCode(phone.value)
let countdownTimer: number | null = null
const switchMode = (nextMode: string) => {
mode.value = nextMode
errorMessage.value = ''
}
const validPhone = (): boolean => {
if (!/^1[3-9][0-9]{9}$/.test(phone.value)) {
errorMessage.value = '请输入正确的 11 位手机号'
return false
}
return true
}
const startCountdown = () => {
countdown.value = 60
if (countdownTimer != null) clearInterval(countdownTimer)
countdownTimer = setInterval(() => {
if (countdown.value <= 1) {
countdown.value = 0
if (countdownTimer != null) clearInterval(countdownTimer)
countdownTimer = null
} else {
countdown.value -= 1
}
}, 1000)
}
const finishLogin = (token: string) => {
saveSession(token, phone.value)
uni.redirectTo({ url: '/pages/home/home' })
}
const resendSms = async () => {
if (loading.value || countdown.value > 0 || !validPhone()) return
loading.value = true
errorMessage.value = ''
try {
if (!(await requestSmsCode(phone.value))) throw '验证码发送失败,请稍后重试'
startCountdown()
uni.showToast({ title: '验证码已重新发送', icon: 'none' })
} catch (error) {
errorMessage.value = error == null ? '验证码发送失败,请稍后重试' : error.toString()
} finally {
loading.value = false
}
}
const login = async () => {
errorMessage.value = ''
if (!agreed.value) { errorMessage.value = '请先同意用户协议'; return }
if (!validPhone()) return
if (loading.value) return
loading.value = true
try {
if (mode.value == 'phone') {
if (!smsSent.value) {
if (countdown.value > 0) return
const sent = await requestSmsCode(phone.value)
if (!sent) throw '验证码发送失败,请稍后重试'
smsSent.value = true
startCountdown()
uni.showToast({ title: '验证码已发送', icon: 'none' })
} else {
if (!/^[0-9]{6}$/.test(smsCode.value)) throw '请输入 6 位数字验证码'
finishLogin(await loginWithSms(phone.value, smsCode.value))
}
} else {
if (password.value.length < 6) throw '密码至少需要 6 位'
finishLogin(await loginWithPassword(phone.value, password.value))
}
} catch (error) {
errorMessage.value = error == null ? '登录失败,请稍后重试' : error.toString()
} finally {
loading.value = false
}
}
onUnmounted(() => { if (countdownTimer != null) clearInterval(countdownTimer) })
</script>
<style>
@@ -46,7 +122,10 @@
.input-box { height: 96rpx; margin-top: 28rpx; border: 2rpx solid #dfe7f2; border-radius: 14rpx; padding: 0 24rpx; flex-direction: row; align-items: center; background-color: #fbfdff; }
.input-prefix { font-size: 25rpx; color: #3c4a61; margin-right: 24rpx; }
.input-box input { flex: 1; font-size: 28rpx; color: #17243c; }
.countdown { min-width: 90rpx; text-align: right; color: #0868f7; font-size: 24rpx; }
.error-message { display: block; margin-top: 16rpx; color: #e55353; font-size: 23rpx; }
.login-button { height: 92rpx; line-height: 92rpx; margin-top: 32rpx; border-radius: 14rpx; color: white; font-size: 29rpx; font-weight: 700; background-color: #0868f7; box-shadow: 0 16rpx 30rpx rgba(8, 104, 247, .2); }
.login-button[disabled] { opacity: .6; }
.other-login { margin-top: 62rpx; flex-direction: row; align-items: center; justify-content: center; color: #a0a9b7; font-size: 22rpx; }
.line { height: 1rpx; background-color: #e5eaf1; width: 120rpx; margin: 0 20rpx; }
.social-row { flex-direction: row; justify-content: center; margin-top: 40rpx; }