From 34bb7cd750e9a4f79a17b7d4058fbb5c854e5b5b Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期六, 10 十月 2026 17:56:20 +0800
Subject: [PATCH] feat(user-miniapp, partner-miniapp): 实现用户小程序登录注册全流程,优化登录交互
---
hugray-user-miniapp/static/logo.png | 0
hugray-user-miniapp/pages/index/index.vue | 60 --
hugray-user-miniapp/pages/login/index.vue | 376 ++++++++++++++++++++
hugray-user-miniapp/pages/login/register.vue | 360 ++++++++++++++++++++
hugray-user-miniapp/pages/login/register-success.vue | 91 +++++
hugray-user-miniapp/pages.json | 24 +
hugray-partner-miniapp/pages/login/index.vue | 106 +++++
hugray-user-miniapp/api/index.js | 26 +
8 files changed, 994 insertions(+), 49 deletions(-)
diff --git a/hugray-partner-miniapp/pages/login/index.vue b/hugray-partner-miniapp/pages/login/index.vue
index 48905c8..550cb34 100644
--- a/hugray-partner-miniapp/pages/login/index.vue
+++ b/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>
diff --git a/hugray-user-miniapp/api/index.js b/hugray-user-miniapp/api/index.js
index fee1ee9..b2b6fb5 100644
--- a/hugray-user-miniapp/api/index.js
+++ b/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() {
diff --git a/hugray-user-miniapp/pages.json b/hugray-user-miniapp/pages.json
index 869105d..6129992 100644
--- a/hugray-user-miniapp/pages.json
+++ b/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"
},
diff --git a/hugray-user-miniapp/pages/index/index.vue b/hugray-user-miniapp/pages/index/index.vue
index ec0ec26..8d44327 100644
--- a/hugray-user-miniapp/pages/index/index.vue
+++ b/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>
diff --git a/hugray-user-miniapp/pages/login/index.vue b/hugray-user-miniapp/pages/login/index.vue
new file mode 100644
index 0000000..04a2e76
--- /dev/null
+++ b/hugray-user-miniapp/pages/login/index.vue
@@ -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>
diff --git a/hugray-user-miniapp/pages/login/register-success.vue b/hugray-user-miniapp/pages/login/register-success.vue
new file mode 100644
index 0000000..115e43e
--- /dev/null
+++ b/hugray-user-miniapp/pages/login/register-success.vue
@@ -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>
diff --git a/hugray-user-miniapp/pages/login/register.vue b/hugray-user-miniapp/pages/login/register.vue
new file mode 100644
index 0000000..eb9c012
--- /dev/null
+++ b/hugray-user-miniapp/pages/login/register.vue
@@ -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>
diff --git a/hugray-user-miniapp/static/logo.png b/hugray-user-miniapp/static/logo.png
index b5771e2..f60e226 100644
--- a/hugray-user-miniapp/static/logo.png
+++ b/hugray-user-miniapp/static/logo.png
Binary files differ
--
Gitblit v1.7.1