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