feat(user-miniapp, partner-miniapp): 实现用户小程序登录注册全流程,优化登录交互

- 新增用户小程序登录、注册、注册成功页面及对应业务API
- 重构用户小程序首页,添加登录态自动校验跳转逻辑
- 更新pages.json配置,新增页面路由并调整导航栏标题
- 修复伙伴小程序登录页密码框placeholder文案错误
- 为两端登录页添加未勾选协议的二次确认弹窗
- 更新用户小程序logo图片
3个文件已添加
5个文件已修改
1043 ■■■■■ 已修改文件
hugray-partner-miniapp/pages/login/index.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/api/index.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages.json 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/index/index.vue 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/login/index.vue 376 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/login/register-success.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/pages/login/register.vue 360 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
hugray-user-miniapp/static/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
hugray-partner-miniapp/pages/login/index.vue
@@ -39,7 +39,7 @@
                        v-model="form.password"
                        class="field-input"
                        password
                        placeholder="请输入验证码"
                        placeholder="请输入密码"
                        placeholder-style="color:#b8bec7;font-size:28rpx"
                    />
                </view>
@@ -66,6 +66,20 @@
        <view class="bottom-bar">
            <view class="login-btn" @tap="_handleLogin">登录</view>
        </view>
        <!-- 未勾选协议时的确认弹窗 -->
        <view v-if="showAgreementModal" class="modal-mask">
            <view class="modal-box">
                <text class="modal-title">用户协议与隐私权限</text>
                <text class="modal-content">请确认已同意《用户协议》、《隐私与权限协议》</text>
                <view class="modal-actions">
                    <view class="modal-btn modal-btn--cancel" @tap="_handleAgreementCancel">取消</view>
                    <view class="modal-btn modal-btn--confirm" @tap="_handleAgreementConfirm">
                        我已阅读并同意
                    </view>
                </view>
            </view>
        </view>
    </view>
</template>
@@ -88,6 +102,9 @@
// 登录请求进行中标记,防止重复提交
const logging = ref(false)
// 未勾选协议时确认弹窗的显示状态
const showAgreementModal = ref(false)
// 状态栏高度(px),用于自定义导航栏顶部留白
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
@@ -97,17 +114,34 @@
const _validateForm = () => {
    if (!form.username.trim()) return '请输入账号'
    if (!form.password.trim()) return '请输入密码'
    if (!agreed.value) return '请先阅读并同意用户协议'
    return ''
}
/** 执行登录:校验 → 调接口 → 写入 token → 跳转首页 */
/** 关闭协议确认弹窗 */
const _handleAgreementCancel = () => {
    showAgreementModal.value = false
}
/** 同意协议:自动勾选并继续登录 */
const _handleAgreementConfirm = () => {
    showAgreementModal.value = false
    agreed.value = true
    _handleLogin()
}
/** 执行登录:校验 → 协议确认 → 调接口 → 写入 token → 跳转首页 */
const _handleLogin = async () => {
    if (logging.value) return
    const errorMsg = _validateForm()
    if (errorMsg) {
        uni.showToast({ title: errorMsg, icon: 'none' })
        return
    }
    // 未勾选协议:先弹窗二次确认,用户同意后再继续登录
    if (!agreed.value) {
        showAgreementModal.value = true
        return
    }
@@ -330,4 +364,70 @@
    font-size: 32rpx;
    font-weight: 600;
}
/* ======================== 未勾选协议确认弹窗 ======================== */
/* 遮罩层:全屏半透明,居中承载弹窗 */
.modal-mask {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 999;
}
.modal-box {
    width: 560rpx;
    background-color: #ffffff;
    border-radius: 16rpx;
    overflow: hidden;
}
/* 标题:绿色居中,下方带分隔线 */
.modal-title {
    display: block;
    padding: 40rpx 32rpx 30rpx;
    font-size: 34rpx;
    font-weight: 600;
    color: #8bc34a;
    text-align: center;
    border-bottom: 1rpx solid #ececec;
}
/* 正文:灰色居中的提示文案 */
.modal-content {
    display: block;
    padding: 44rpx 48rpx;
    font-size: 28rpx;
    line-height: 42rpx;
    color: #9aa1ab;
    text-align: center;
}
/* 按钮行:取消 / 我已阅读并同意 各占一半 */
.modal-actions {
    display: flex;
    align-items: center;
    padding-bottom: 24rpx;
}
.modal-btn {
    flex: 1;
    text-align: center;
    font-size: 30rpx;
    font-weight: 600;
}
.modal-btn--cancel {
    color: #1f2329;
}
.modal-btn--confirm {
    color: #8bc34a;
}
</style>
hugray-user-miniapp/api/index.js
@@ -23,6 +23,32 @@
}
/**
 * 手机号一键登录(微信手机号快速验证,凭证换取登录态)
 * @param {Object} data { code } 手机号快速验证凭证
 */
export function phoneLogin(data) {
    return post('/auth/phone/login', data, { auth: false })
}
/**
 * 发送短信验证码
 * @param {String} phone 手机号
 * @param {String} [scene] 业务场景,register=注册 / reset=重置密码,默认注册
 * 说明:注册场景下后端会先判断手机号是否已注册,已注册返回「手机号已注册,请前往登录」
 */
export function sendSmsCode(phone, scene = 'register') {
    return post('/auth/sms/code', { phone, scene }, { auth: false })
}
/**
 * 用户注册(推荐码必填,无推荐码无法注册)
 * @param {Object} data { phone, code, referralCode }
 */
export function register(data) {
    return post('/auth/register', data, { auth: false })
}
/**
 * 退出登录
 */
export function logout() {
hugray-user-miniapp/pages.json
@@ -3,13 +3,33 @@
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "uni-app"
                "navigationBarTitleText": ""
            }
        },
        {
            "path": "pages/login/index",
            "style": {
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/login/register",
            "style": {
                "navigationStyle": "custom"
            }
        },
        {
            "path": "pages/login/register-success",
            "style": {
                "navigationBarTitleText": "注册成功",
                "navigationBarBackgroundColor": "#ffffff",
                "navigationBarTextStyle": "black"
            }
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "uni-app",
        "navigationBarTitleText": "",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    },
hugray-user-miniapp/pages/index/index.vue
@@ -1,52 +1,24 @@
<template>
    <view class="content">
        <image class="logo" src="/static/logo.png"></image>
        <view class="text-area">
            <text class="title">{{title}}</text>
        </view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                title: 'Hello'
            }
        },
        onLoad() {
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { getToken } from '@/utils/request.js'
import config from '@/config/index.js'
        },
        methods: {
/**
 * 校验登录态:本地无 token 时重定向至登录页
 */
const _checkLoginStatus = () => {
    if (getToken()) return
    uni.reLaunch({ url: config.loginPage })
}
        }
    }
// 每次页面显示均校验登录态,避免退出登录后仍停留在首页
onShow(() => {
    _checkLoginStatus()
})
</script>
<style>
    .content {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
    .logo {
        height: 200rpx;
        width: 200rpx;
        margin-top: 200rpx;
        margin-left: auto;
        margin-right: auto;
        margin-bottom: 50rpx;
    }
    .text-area {
        display: flex;
        justify-content: center;
    }
    .title {
        font-size: 36rpx;
        color: #8f8f94;
    }
<style scoped lang="scss">
</style>
hugray-user-miniapp/pages/login/index.vue
New file
@@ -0,0 +1,376 @@
<template>
    <view class="login-page">
        <!-- 顶部品牌区:深蓝底承载品牌 logo 与欢迎语 -->
        <view class="hero" :style="{ paddingTop: statusBarHeight + 'px' }">
            <view class="nav-inner">
                <image class="brand-logo" src="/static/logo.png" mode="heightFix" />
            </view>
            <view class="welcome-block">
                <text class="welcome-title">欢迎使用</text>
                <view class="brand-row">
                    <text class="brand-name">代谢</text>
                    <text class="brand-num">300</text>
                </view>
            </view>
        </view>
        <!-- 底部主体区:协议勾选 + 手机号一键登录 -->
        <view class="login-body">
            <!-- 协议勾选 -->
            <view class="agreement-row">
                <view
                    class="checkbox"
                    :class="{ 'checkbox--checked': agreed }"
                    @tap="agreed = !agreed"
                ></view>
                <text class="agreement-text">我已阅读并同意</text>
                <text class="agreement-link" @tap.stop="_openAgreement('用户协议')">《用户协议》</text>
                <text class="agreement-text">、</text>
                <text class="agreement-link" @tap.stop="_openAgreement('隐私与权限协议')">《隐私与权限协议》</text>
            </view>
            <!-- 手机号一键登录:未勾选协议时 open-type 置空,由点击事件拦截并弹窗提示 -->
            <button
                class="login-btn"
                :open-type="agreed ? 'getPhoneNumber' : ''"
                @tap="_handleLoginTap"
                @getphonenumber="_handleGetPhoneNumber"
            >
                手机号一键登录
            </button>
            <!-- 注册入口:跳转注册页 -->
            <view class="register-entry">
                <text class="register-tip">还没有账号?</text>
                <text class="register-link" @tap="_goRegister">立即注册</text>
            </view>
        </view>
        <!-- 未勾选协议时的确认弹窗 -->
        <view v-if="showAgreementModal" class="modal-mask">
            <view class="modal-box">
                <text class="modal-title">用户协议与隐私权限</text>
                <text class="modal-content">请确认已同意《用户协议》、《隐私与权限协议》</text>
                <view class="modal-actions">
                    <view class="modal-btn modal-btn--cancel" @tap="_handleAgreementCancel">取消</view>
                    <view class="modal-btn modal-btn--confirm" @tap="_handleAgreementConfirm">
                        我已阅读并同意
                    </view>
                </view>
            </view>
        </view>
    </view>
</template>
<script setup>
import { ref } from 'vue'
import { phoneLogin } from '@/api/index.js'
import { setToken } from '@/utils/request.js'
// ======================== 数据定义区域 ========================
// 是否已勾选同意用户协议
const agreed = ref(false)
// 登录请求进行中标记,防止重复提交
const logging = ref(false)
// 未勾选协议时确认弹窗的显示状态
const showAgreementModal = ref(false)
// 状态栏高度(px),用于顶部品牌区留白
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
// ======================== 方法定义区域 ========================
/** 点击登录:未勾选协议时弹窗二次确认 */
const _handleLoginTap = () => {
    if (!agreed.value) {
        showAgreementModal.value = true
    }
}
/** 关闭协议确认弹窗 */
const _handleAgreementCancel = () => {
    showAgreementModal.value = false
}
/**
 * 同意协议:自动勾选(手机号授权必须由用户点击按钮触发,
 * 故此处仅提示用户再次点击按钮完成登录)
 */
const _handleAgreementConfirm = () => {
    showAgreementModal.value = false
    agreed.value = true
    uni.showToast({ title: '请再次点击「手机号一键登录」', icon: 'none' })
}
/**
 * 微信手机号授权回调:拿到手机号凭证后换取登录态
 * @param {Object} e 授权结果,e.detail.code 为手机号快速验证凭证
 */
const _handleGetPhoneNumber = async (e) => {
    if (logging.value) return
    const detail = (e && e.detail) || {}
    // 用户拒绝授权或未取得手机号凭证
    if (!detail.code) {
        uni.showToast({ title: '已取消手机号授权', icon: 'none' })
        return
    }
    logging.value = true
    try {
        const { data } = await phoneLogin({ code: detail.code })
        const token = (data && (data.token || data.accessToken)) || ''
        if (!token) throw new Error('登录失败:未获取到登录凭证')
        setToken(token)
        uni.reLaunch({ url: '/pages/index/index' })
    } catch (err) {
        uni.showToast({ title: err.message || '登录失败', icon: 'none' })
    } finally {
        logging.value = false
    }
}
/** 跳转注册页 */
const _goRegister = () => {
    uni.navigateTo({ url: '/pages/login/register' })
}
/** 查看协议(协议页尚未开发,先以提示占位) */
const _openAgreement = (name) => {
    uni.showToast({ title: name + '页面开发中', icon: 'none' })
}
</script>
<style scoped lang="scss">
/* 页面最外层:纵向弹性布局,撑满整屏 */
.login-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background-color: #ffffff;
}
/* 顶部品牌区:深蓝底 + 品牌 logo(底色与 logo 背景 #0b3f6f 一致,使 logo 无缝融入) */
.hero {
    min-height: 38vh;
    display: flex;
    flex-direction: column;
    background-color: #0b3f6f;
    /* 底部留出间距,避免欢迎语贴近色块下边缘 */
    padding-bottom: 90rpx;
    box-sizing: border-box;
}
/* 品牌行:固定高度,与小程序右上角胶囊高度对齐 */
.nav-inner {
    height: 88rpx;
    display: flex;
    align-items: center;
    padding-left: 28rpx;
    box-sizing: border-box;
}
.brand-logo {
    height: 56rpx;
    display: block;
}
/* 欢迎语与品牌名:margin-top 自动撑开上方空间,使其贴近品牌区底部 */
.welcome-block {
    margin-top: auto;
    padding: 0 40rpx;
}
.welcome-title {
    display: block;
    font-size: 44rpx;
    font-weight: 700;
    color: #ffffff;
    line-height: 62rpx;
}
.brand-row {
    display: flex;
    align-items: flex-end;
    margin-top: 24rpx;
}
.brand-name {
    font-size: 44rpx;
    font-weight: 700;
    color: #ffffff;
    line-height: 62rpx;
}
/* 品牌数字:绿色大号斜体,突出代谢 300 品牌标识 */
.brand-num {
    margin-left: 16rpx;
    font-size: 96rpx;
    font-weight: 800;
    font-style: italic;
    color: #8bc34a;
    line-height: 96rpx;
}
/* 主体区:承载协议勾选与登录按钮 */
.login-body {
    flex: 1;
    padding: 300rpx 32rpx 0;
    box-sizing: border-box;
}
/* 协议勾选行:整体居中排列 */
.agreement-row {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    margin-bottom: 44rpx;
}
.checkbox {
    position: relative;
    width: 32rpx;
    height: 32rpx;
    border: 2rpx solid #c0c4cc;
    border-radius: 4rpx;
    margin-right: 14rpx;
    flex-shrink: 0;
    box-sizing: border-box;
}
.checkbox--checked {
    background-color: #8bc34a;
    border-color: #8bc34a;
}
/* 勾选后的白色对勾 */
.checkbox--checked::after {
    content: '';
    position: absolute;
    left: 9rpx;
    top: 3rpx;
    width: 8rpx;
    height: 16rpx;
    border-right: 3rpx solid #ffffff;
    border-bottom: 3rpx solid #ffffff;
    transform: rotate(45deg);
}
.agreement-text {
    font-size: 24rpx;
    color: #6b7280;
}
.agreement-link {
    font-size: 24rpx;
    color: #1a73e8;
}
/* 手机号一键登录按钮:绿色圆角块,两侧留边距 */
.login-btn {
    margin: 0 32rpx;
    height: 96rpx;
    line-height: 96rpx;
    border: none;
    border-radius: 16rpx;
    background-color: #8bc34a;
    color: #ffffff;
    font-size: 32rpx;
    font-weight: 600;
}
/* 覆盖小程序 button 默认的边框伪元素 */
.login-btn::after {
    border: none;
}
/* 注册入口:居中排列的提示文案 + 链接 */
.register-entry {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 40rpx;
}
.register-tip {
    font-size: 26rpx;
    color: #6b7280;
}
.register-link {
    margin-left: 8rpx;
    font-size: 26rpx;
    color: #1a73e8;
}
/* ======================== 未勾选协议确认弹窗 ======================== */
/* 遮罩层:全屏半透明,居中承载弹窗 */
.modal-mask {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 999;
}
.modal-box {
    width: 560rpx;
    background-color: #ffffff;
    border-radius: 16rpx;
    overflow: hidden;
}
/* 标题:绿色居中,下方带分隔线 */
.modal-title {
    display: block;
    padding: 40rpx 32rpx 30rpx;
    font-size: 34rpx;
    font-weight: 600;
    color: #8bc34a;
    text-align: center;
    border-bottom: 1rpx solid #ececec;
}
/* 正文:灰色居中的提示文案 */
.modal-content {
    display: block;
    padding: 44rpx 48rpx;
    font-size: 28rpx;
    line-height: 42rpx;
    color: #9aa1ab;
    text-align: center;
}
/* 按钮行:取消 / 我已阅读并同意 各占一半 */
.modal-actions {
    display: flex;
    align-items: center;
    padding-bottom: 24rpx;
}
.modal-btn {
    flex: 1;
    text-align: center;
    font-size: 30rpx;
    font-weight: 600;
}
.modal-btn--cancel {
    color: #1f2329;
}
.modal-btn--confirm {
    color: #8bc34a;
}
</style>
hugray-user-miniapp/pages/login/register-success.vue
New file
@@ -0,0 +1,91 @@
<template>
    <view class="success-page">
        <!-- 结果区:绿色圆形对勾 + 成功文案 -->
        <view class="result-block">
            <view class="success-icon"></view>
            <text class="success-text">您的账号已成功注册</text>
        </view>
        <!-- 底部通栏返回登录按钮 -->
        <view class="bottom-bar">
            <view class="submit-btn" @tap="_handleBackLogin">返回登录</view>
        </view>
    </view>
</template>
<script setup>
// ======================== 方法定义区域 ========================
/** 返回登录页:清空页面栈,重新进入登录页 */
const _handleBackLogin = () => {
    uni.reLaunch({ url: '/pages/login/index' })
}
</script>
<style scoped lang="scss">
/* 页面最外层:纵向弹性布局,撑满整屏 */
.success-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background-color: #ffffff;
    box-sizing: border-box;
}
/* 结果区:内容水平居中,与顶部导航拉开距离 */
.result-block {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 90rpx 0 50rpx;
}
/* 绿色圆形底 + 白色对勾(对勾由两条边框旋转 45° 拼成) */
.success-icon {
    position: relative;
    width: 160rpx;
    height: 160rpx;
    border-radius: 50%;
    background-color: #8bc34a;
}
.success-icon::after {
    content: '';
    position: absolute;
    left: 54rpx;
    top: 30rpx;
    width: 40rpx;
    height: 72rpx;
    border-right: 8rpx solid #ffffff;
    border-bottom: 8rpx solid #ffffff;
    transform: rotate(45deg);
}
.success-text {
    margin-top: 40rpx;
    font-size: 30rpx;
    color: #1f2329;
}
/* 底部按钮区:白底 + 左右留边距,底部叠加安全区 */
.bottom-bar {
    margin-top: auto;
    padding: 20rpx 32rpx;
    padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
    padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
    background-color: #ffffff;
}
/* 返回登录按钮:绿色实心,圆角与登录页「手机号一键登录」保持一致 */
.submit-btn {
    height: 96rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 16rpx;
    background-color: #8bc34a;
    color: #ffffff;
    font-size: 32rpx;
    font-weight: 600;
}
</style>
hugray-user-miniapp/pages/login/register.vue
New file
@@ -0,0 +1,360 @@
<template>
    <view class="register-page">
        <!-- 顶部品牌导航栏:深蓝底 + 品牌 logo(与登录页保持一致) -->
        <view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
            <view class="nav-inner">
                <image class="brand-logo" src="/static/logo.png" mode="heightFix" />
            </view>
        </view>
        <!-- 表单区 -->
        <view class="form-area">
            <!-- 手机号 -->
            <view class="field-block">
                <text class="field-label">手机号</text>
                <view class="input-wrap">
                    <view class="field-icon icon-phone"></view>
                    <input
                        v-model="form.phone"
                        class="field-input"
                        type="number"
                        maxlength="11"
                        placeholder="请输入账号"
                        placeholder-style="color:#b8bec7;font-size:28rpx"
                    />
                </view>
            </view>
            <!-- 验证码 -->
            <view class="field-block">
                <text class="field-label">验证码</text>
                <view class="code-row">
                    <view class="input-wrap input-wrap--flex">
                        <view class="field-icon icon-shield"></view>
                        <input
                            v-model="form.code"
                            class="field-input"
                            type="number"
                            maxlength="6"
                            placeholder="请输入验证码"
                            placeholder-style="color:#b8bec7;font-size:28rpx"
                        />
                    </view>
                    <!-- 获取验证码:倒计时期间禁用,显示剩余秒数 -->
                    <view
                        class="code-btn"
                        :class="{ 'code-btn--disabled': counting }"
                        @tap="_handleSendCode"
                    >
                        {{ counting ? countdown + 's' : '获取验证码' }}
                    </view>
                </view>
            </view>
            <!-- 推荐码(必填,无推荐码无法注册) -->
            <view class="field-block">
                <text class="field-label">推荐码</text>
                <view class="input-wrap">
                    <view class="field-icon icon-grid"></view>
                    <input
                        v-model="form.referralCode"
                        class="field-input"
                        maxlength="20"
                        placeholder="请输入推荐人码"
                        placeholder-style="color:#b8bec7;font-size:28rpx"
                    />
                </view>
            </view>
            <!-- 已有账号:右对齐,点击返回登录页 -->
            <view class="has-account" @tap="_handleBackLogin">
                <text class="has-account-text">已有账号</text>
                <text class="has-account-link">去登录</text>
            </view>
        </view>
        <!-- 底部通栏注册按钮 -->
        <view class="bottom-bar">
            <view class="submit-btn" @tap="_handleRegister">注册</view>
        </view>
    </view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onUnload } from '@dcloudio/uni-app'
import { sendSmsCode, register } from '@/api/index.js'
// ======================== 数据定义区域 ========================
// 注册表单数据
const form = reactive({
    phone: '',
    code: '',
    referralCode: ''
})
// 验证码倒计时剩余秒数(0 表示未在倒计时)
const countdown = ref(0)
// 是否处于倒计时状态(用于禁用「获取验证码」按钮)
const counting = computed(() => countdown.value > 0)
// 注册请求进行中标记,防止重复提交
const registering = ref(false)
// 倒计时定时器句柄
let countdownTimer = null
// 状态栏高度(px),用于顶部品牌区留白
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
// ======================== 方法定义区域 ========================
/** 校验手机号格式 */
const _isValidPhone = (phone) => /^1[3-9]\d{9}$/.test(phone)
/** 启动 30 秒倒计时,期间禁用「获取验证码」,30 秒后才可再次发送 */
const _startCountdown = () => {
    countdown.value = 30
    countdownTimer = setInterval(() => {
        countdown.value -= 1
        if (countdown.value <= 0) {
            clearInterval(countdownTimer)
            countdownTimer = null
        }
    }, 1000)
}
/**
 * 获取验证码:未输入手机号 / 格式错误时前端直接提示;
 * 手机号已注册的情况由后端返回「手机号已注册,请前往登录」,此处统一展示错误信息
 */
const _handleSendCode = async () => {
    if (counting.value) return
    const phone = form.phone.trim()
    if (!phone) {
        uni.showToast({ title: '请输入手机号', icon: 'none' })
        return
    }
    if (!_isValidPhone(phone)) {
        uni.showToast({ title: '请输入正确手机号', icon: 'none' })
        return
    }
    try {
        await sendSmsCode(phone)
        uni.showToast({ title: '验证码已发送', icon: 'none' })
        _startCountdown()
    } catch (e) {
        uni.showToast({ title: e.message || '验证码发送失败', icon: 'none' })
    }
}
/** 表单校验,返回校验失败原因(通过则返回空串) */
const _validateForm = () => {
    if (!form.phone.trim()) return '请输入手机号'
    if (!_isValidPhone(form.phone.trim())) return '请输入正确手机号'
    if (!form.code.trim()) return '请输入验证码'
    // 推荐码为必填,没有推荐码无法注册
    if (!form.referralCode.trim()) return '请输入推荐人码'
    return ''
}
/** 提交注册:校验 → 调接口 → 跳转注册成功页 */
const _handleRegister = async () => {
    if (registering.value) return
    const errorMsg = _validateForm()
    if (errorMsg) {
        uni.showToast({ title: errorMsg, icon: 'none' })
        return
    }
    registering.value = true
    try {
        await register({
            phone: form.phone.trim(),
            code: form.code.trim(),
            referralCode: form.referralCode.trim()
        })
        // 替换当前页,注册成功页返回时直接回到登录页
        uni.redirectTo({ url: '/pages/login/register-success' })
    } catch (e) {
        uni.showToast({ title: e.message || '注册失败', icon: 'none' })
    } finally {
        registering.value = false
    }
}
/** 返回登录页 */
const _handleBackLogin = () => {
    uni.navigateBack()
}
// 页面卸载时清理倒计时,避免定时器泄漏
onUnload(() => {
    if (countdownTimer) clearInterval(countdownTimer)
})
</script>
<style scoped lang="scss">
/* 页面最外层:纵向弹性布局,撑满整屏 */
.register-page {
    display: flex;
    flex-direction: column;
    height: 100vh;
    background-color: #ffffff;
}
/* 顶部品牌导航栏:深蓝底,与登录页品牌区一致 */
.nav-bar {
    background-color: #0b3f6f;
}
.nav-inner {
    height: 88rpx;
    display: flex;
    align-items: center;
    padding-left: 28rpx;
    box-sizing: border-box;
}
.brand-logo {
    height: 56rpx;
    display: block;
}
.form-area {
    flex: 1;
    padding: 40rpx 28rpx 0;
    box-sizing: border-box;
}
/* 单个输入项 */
.field-block {
    margin-bottom: 40rpx;
}
.field-label {
    display: block;
    font-size: 28rpx;
    color: #1f2329;
    margin-bottom: 20rpx;
}
/* 带图标的输入框 */
.input-wrap {
    display: flex;
    align-items: center;
    height: 96rpx;
    padding: 0 24rpx;
    border: 1rpx solid #dcdfe6;
    border-radius: 8rpx;
    box-sizing: border-box;
}
/* 验证码行:输入框自适应占满剩余宽度 */
.code-row {
    display: flex;
    align-items: center;
}
.input-wrap--flex {
    flex: 1;
    min-width: 0;
    margin-right: 20rpx;
}
.field-icon {
    width: 44rpx;
    height: 44rpx;
    margin-right: 20rpx;
    flex-shrink: 0;
    background-repeat: no-repeat;
    background-position: center center;
    background-size: 100% 100%;
}
/* 手机号图标:手机外框 + 底部听筒线 */
.icon-phone {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NCA0NCI+CiAgPHJlY3QgeD0iMTIuNSIgeT0iNS41IiB3aWR0aD0iMTkiIGhlaWdodD0iMzMiIHJ4PSI0LjUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzBiM2Y2ZiIgc3Ryb2tlLXdpZHRoPSIzIi8+CiAgPGxpbmUgeDE9IjE4LjUiIHkxPSIzMi41IiB4Mj0iMjUuNSIgeTI9IjMyLjUiIHN0cm9rZT0iIzBiM2Y2ZiIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+Cg==');
}
/* 验证码图标:上凸尖角盾牌 + 四宫格棋盘 */
.icon-shield {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NCA0NCI+CiAgPGRlZnM+CiAgICA8Y2xpcFBhdGggaWQ9InNjIj4KICAgICAgPHBhdGggZD0iTTIyIDkgTDMzIDE3LjUgTDMzIDI1IEMzMyAzMS41IDI3LjUgMzYgMjIgMzggQzE2LjUgMzYgMTEgMzEuNSAxMSAyNSBMMTEgMTcuNSBaIi8+CiAgICA8L2NsaXBQYXRoPgogIDwvZGVmcz4KICA8cGF0aCBkPSJNMjIgNCBMMzggMTUgTDM4IDI1IEMzOCAzNCAzMCA0MCAyMiA0MiBDMTQgNDAgNiAzNCA2IDI1IEw2IDE1IFoiIGZpbGw9IiMwYjNmNmYiLz4KICA8ZyBjbGlwLXBhdGg9InVybCgjc2MpIj4KICAgIDxyZWN0IHg9IjExIiB5PSI5IiB3aWR0aD0iMjIiIGhlaWdodD0iMjkiIGZpbGw9IiNmZmZmZmYiLz4KICAgIDxyZWN0IHg9IjIyIiB5PSI5IiB3aWR0aD0iMTEiIGhlaWdodD0iMTQuNSIgZmlsbD0iIzBiM2Y2ZiIvPgogICAgPHJlY3QgeD0iMTEiIHk9IjIzLjUiIHdpZHRoPSIxMSIgaGVpZ2h0PSIxNC41IiBmaWxsPSIjMGIzZjZmIi8+CiAgPC9nPgo8L3N2Zz4K');
}
/* 推荐码图标:2×2 宫格 */
.icon-grid {
    background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NCA0NCI+CiAgPHJlY3QgeD0iNiIgeT0iNiIgd2lkdGg9IjEzLjUiIGhlaWdodD0iMTMuNSIgcng9IjIuNSIgZmlsbD0iIzBiM2Y2ZiIvPgogIDxyZWN0IHg9IjI0LjUiIHk9IjYiIHdpZHRoPSIxMy41IiBoZWlnaHQ9IjEzLjUiIHJ4PSIyLjUiIGZpbGw9IiMwYjNmNmYiLz4KICA8cmVjdCB4PSI2IiB5PSIyNC41IiB3aWR0aD0iMTMuNSIgaGVpZ2h0PSIxMy41IiByeD0iMi41IiBmaWxsPSIjMGIzZjZmIi8+CiAgPHJlY3QgeD0iMjQuNSIgeT0iMjQuNSIgd2lkdGg9IjEzLjUiIGhlaWdodD0iMTMuNSIgcng9IjIuNSIgZmlsbD0iIzBiM2Y2ZiIvPgo8L3N2Zz4K');
}
.field-input {
    flex: 1;
    height: 100%;
    font-size: 28rpx;
    color: #1f2329;
    min-width: 0;
}
/* 获取验证码按钮:绿色实心,倒计时期间置灰不可点 */
.code-btn {
    width: 190rpx;
    height: 96rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8rpx;
    background-color: #8bc34a;
    color: #ffffff;
    font-size: 28rpx;
    flex-shrink: 0;
}
.code-btn--disabled {
    background-color: #c3dd9d;
}
/* 已有账号:右对齐,点击返回登录 */
.has-account {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    margin-top: 8rpx;
}
.has-account-text {
    font-size: 28rpx;
    color: #6b7280;
}
.has-account-link {
    margin-left: 8rpx;
    font-size: 28rpx;
    color: #1a73e8;
}
/* 底部按钮区:白底 + 左右留边距,底部叠加安全区 */
.bottom-bar {
    padding: 20rpx 32rpx;
    padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
    padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
    background-color: #ffffff;
}
/* 注册按钮:绿色实心,圆角与登录页「手机号一键登录」保持一致 */
.submit-btn {
    height: 96rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 16rpx;
    background-color: #8bc34a;
    color: #ffffff;
    font-size: 32rpx;
    font-weight: 600;
}
</style>
hugray-user-miniapp/static/logo.png