|
|
@@ -0,0 +1,532 @@
|
|
|
+<template>
|
|
|
+ <view class="login-page">
|
|
|
+ <nav-bar title="注册" bgColor="transparent" color="#fff" :showBack="false"></nav-bar>
|
|
|
+
|
|
|
+ <!-- 顶部渐变装饰区 -->
|
|
|
+ <view class="hero-bg">
|
|
|
+ <view class="deco-circle c1"></view>
|
|
|
+ <view class="deco-circle c2"></view>
|
|
|
+ <view class="deco-circle c3"></view>
|
|
|
+
|
|
|
+ <!-- 返回按钮 -->
|
|
|
+ <view class="back-btn" @click="onClickLeft">
|
|
|
+ <uni-icons type="left" size="22" color="#fff"></uni-icons>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- Logo 区域 -->
|
|
|
+ <view class="hero-content">
|
|
|
+ <text class="brand-desc">开启您的好萌友服务之旅</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 表单白色卡片区 -->
|
|
|
+ <view class="form-card">
|
|
|
+
|
|
|
+ <!-- 头像上传 -->
|
|
|
+ <view class="avatar-upload-wrap" @click="onChooseAvatar">
|
|
|
+ <image v-if="avatar" :src="avatar" class="avatar-image" mode="aspectFill"></image>
|
|
|
+ <view v-else class="avatar-placeholder">
|
|
|
+ <uni-icons type="camera-filled" size="32" color="#ccc"></uni-icons>
|
|
|
+ <text class="avatar-text">上传头像</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 商户名称 -->
|
|
|
+ <view class="input-group">
|
|
|
+ <view class="input-icon-wrap">
|
|
|
+ <uni-icons type="shop" size="18" color="#ffc837"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <input class="custom-input" v-model="companyName" placeholder="请输入商户名称"
|
|
|
+ placeholder-class="input-placeholder" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 用户账号 -->
|
|
|
+ <view class="input-group">
|
|
|
+ <view class="input-icon-wrap">
|
|
|
+ <uni-icons type="person" size="18" color="#ffc837"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <input class="custom-input" v-model="username" placeholder="请输入用户账号"
|
|
|
+ placeholder-class="input-placeholder" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 姓名 -->
|
|
|
+ <view class="input-group">
|
|
|
+ <view class="input-icon-wrap">
|
|
|
+ <uni-icons type="staff" size="18" color="#ffc837"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <input class="custom-input" v-model="name" placeholder="请输入姓名"
|
|
|
+ placeholder-class="input-placeholder" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 邮箱 -->
|
|
|
+ <view class="input-group">
|
|
|
+ <view class="input-icon-wrap">
|
|
|
+ <uni-icons type="email" size="18" color="#ffc837"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <input class="custom-input" v-model="email" placeholder="请输入邮箱"
|
|
|
+ placeholder-class="input-placeholder" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 密码 -->
|
|
|
+ <view class="input-group">
|
|
|
+ <view class="input-icon-wrap">
|
|
|
+ <uni-icons type="locked" size="18" color="#ffc837"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <input class="custom-input" v-model="password" type="password" placeholder="请输入密码"
|
|
|
+ placeholder-class="input-placeholder" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 确认密码 -->
|
|
|
+ <view class="input-group">
|
|
|
+ <view class="input-icon-wrap">
|
|
|
+ <uni-icons type="locked" size="18" color="#ffc837"></uni-icons>
|
|
|
+ </view>
|
|
|
+ <input class="custom-input" v-model="confirmPassword" type="password" placeholder="请再次输入密码"
|
|
|
+ placeholder-class="input-placeholder" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 协议勾选 -->
|
|
|
+ <view class="agreement-row">
|
|
|
+ <checkbox-group @change="onCheckChange">
|
|
|
+ <label class="agree-label">
|
|
|
+ <checkbox :checked="checked" color="#ffc837" style="transform: scale(0.7);" />
|
|
|
+ <text class="agree-text">我已阅读并同意</text>
|
|
|
+ <text class="text-link" @click.stop="showAgreement(2)">《隐私政策》</text>
|
|
|
+ <text class="agree-text">和</text>
|
|
|
+ <text class="text-link" @click.stop="showAgreement(4)">《托运协议》</text>
|
|
|
+ </label>
|
|
|
+ </checkbox-group>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 提交注册按钮 -->
|
|
|
+ <button class="login-btn" @click="onSubmit">提交注册申请</button>
|
|
|
+
|
|
|
+ <!-- 返回登录 -->
|
|
|
+ <view class="register-redirect-row">
|
|
|
+ <text class="redirect-hint">已有账号?</text>
|
|
|
+ <text class="redirect-action" @click="goToLogin">立即登录</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 隐私政策/托运协议弹窗 -->
|
|
|
+ <policy-dialog v-model:visible="dialogVisible" :title="dialogTitle" :content="dialogContent"></policy-dialog>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref } from 'vue'
|
|
|
+import navBar from '@/components/nav-bar/index.vue'
|
|
|
+import policyDialog from '@/components/policy-dialog/index.vue'
|
|
|
+import { getAgreement } from '@/api/system/agreement'
|
|
|
+import { register } from '@/api/auth'
|
|
|
+
|
|
|
+const avatar = ref('')
|
|
|
+const companyName = ref('')
|
|
|
+const username = ref('')
|
|
|
+const name = ref('')
|
|
|
+const email = ref('')
|
|
|
+const password = ref('')
|
|
|
+const confirmPassword = ref('')
|
|
|
+const checked = ref(false)
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const dialogTitle = ref('')
|
|
|
+const dialogContent = ref('')
|
|
|
+
|
|
|
+
|
|
|
+const onChooseAvatar = () => {
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 1,
|
|
|
+ success: (res) => {
|
|
|
+ avatar.value = res.tempFilePaths[0]
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const onCheckChange = () => {
|
|
|
+ checked.value = !checked.value
|
|
|
+}
|
|
|
+
|
|
|
+const goToLogin = () => {
|
|
|
+ uni.reLaunch({
|
|
|
+ url: '/pages/login/index'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const onClickLeft = () => {
|
|
|
+ uni.navigateBack({
|
|
|
+ fail: () => {
|
|
|
+ goToLogin()
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const showAgreement = async (agreementId) => {
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '加载中...' })
|
|
|
+ const res = await getAgreement(agreementId)
|
|
|
+
|
|
|
+ if (res && res.title) {
|
|
|
+ dialogTitle.value = res.title || '协议详情'
|
|
|
+ let decodedContent = res.content || '暂无内容'
|
|
|
+ if (res.content) {
|
|
|
+ try {
|
|
|
+ decodedContent = decodeURIComponent(escape(window.atob(res.content)))
|
|
|
+ } catch (e) {
|
|
|
+ decodedContent = res.content
|
|
|
+ }
|
|
|
+ }
|
|
|
+ dialogContent.value = decodedContent
|
|
|
+ dialogVisible.value = true
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: '数据格式异常', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.showToast({ title: '加载失败,请稍后重试', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ uni.hideLoading()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onSubmit = async () => {
|
|
|
+ // 1. 商户名称验证
|
|
|
+ if (!companyName.value) {
|
|
|
+ uni.showToast({ title: '请输入商户名称', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (companyName.value.length < 2 || companyName.value.length > 50) {
|
|
|
+ uni.showToast({ title: '商户名称长度需在 2 到 50 个字符之间', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 用户账号验证
|
|
|
+ if (!username.value) {
|
|
|
+ uni.showToast({ title: '请输入用户账号', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const usernameRegex = /^[a-zA-Z0-9_-]{4,20}$/
|
|
|
+ if (!usernameRegex.test(username.value)) {
|
|
|
+ uni.showToast({ title: '账号必须为 4 到 20 位字母、数字、下划线或减号', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 姓名验证
|
|
|
+ if (!name.value) {
|
|
|
+ uni.showToast({ title: '请输入姓名', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (name.value.length < 2 || name.value.length > 20) {
|
|
|
+ uni.showToast({ title: '姓名长度需在 2 到 20 个字符之间', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 邮箱验证
|
|
|
+ if (!email.value) {
|
|
|
+ uni.showToast({ title: '请输入邮箱', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
|
|
+ if (!emailRegex.test(email.value)) {
|
|
|
+ uni.showToast({ title: '请输入正确的邮箱格式', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 密码验证
|
|
|
+ if (!password.value) {
|
|
|
+ uni.showToast({ title: '请输入密码', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (password.value.length < 6 || password.value.length > 20) {
|
|
|
+ uni.showToast({ title: '密码长度需在 6 到 20 个字符之间', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 确认密码验证
|
|
|
+ if (!confirmPassword.value) {
|
|
|
+ uni.showToast({ title: '请再次输入密码', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (password.value !== confirmPassword.value) {
|
|
|
+ uni.showToast({ title: '两次输入的密码不一致', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 协议勾选验证
|
|
|
+ if (!checked.value) {
|
|
|
+ uni.showToast({ title: '请先阅读并勾选协议', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '提交申请中...' })
|
|
|
+ await register({
|
|
|
+ avatar: avatar.value,
|
|
|
+ companyName: companyName.value,
|
|
|
+ username: username.value,
|
|
|
+ name: name.value,
|
|
|
+ email: email.value,
|
|
|
+ password: password.value,
|
|
|
+ confirmPassword: confirmPassword.value
|
|
|
+ })
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({
|
|
|
+ title: '提交成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ // 缓存商户名称和提交时间,供审核界面展示使用
|
|
|
+ uni.setStorageSync('registered_merchant', companyName.value)
|
|
|
+ uni.setStorageSync('registered_time', new Date().toLocaleString())
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.reLaunch({
|
|
|
+ url: '/pages/login/review'
|
|
|
+ })
|
|
|
+ }, 1000)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Register error:', error)
|
|
|
+ uni.hideLoading()
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.showToast({ title: typeof error === 'string' ? error : '注册提交失败', icon: 'none' })
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.login-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f2f3f7;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.nav-bar) {
|
|
|
+ position: absolute !important;
|
|
|
+}
|
|
|
+
|
|
|
+.back-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: calc(var(--status-bar-height, 44px) + 20rpx);
|
|
|
+ left: 32rpx;
|
|
|
+ width: 76rpx;
|
|
|
+ height: 76rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(255, 255, 255, 0.35);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-bg {
|
|
|
+ background: linear-gradient(150deg, #ffd53f 0%, #ff9500 100%);
|
|
|
+ padding: 0 40rpx 80rpx;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ min-height: 300rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.deco-circle {
|
|
|
+ position: absolute;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(255, 255, 255, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.c1 {
|
|
|
+ width: 400rpx;
|
|
|
+ height: 400rpx;
|
|
|
+ top: -160rpx;
|
|
|
+ right: -120rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.c2 {
|
|
|
+ width: 260rpx;
|
|
|
+ height: 260rpx;
|
|
|
+ top: 80rpx;
|
|
|
+ left: -100rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.c3 {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ bottom: 80rpx;
|
|
|
+ right: 80rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-content {
|
|
|
+ position: relative;
|
|
|
+ z-index: 2;
|
|
|
+}
|
|
|
+
|
|
|
+.brand-desc {
|
|
|
+ display: block;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ letter-spacing: 2rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 56rpx 56rpx 0 0;
|
|
|
+ margin-top: -60rpx;
|
|
|
+ padding: 60rpx 48rpx 60rpx;
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.input-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f6f8;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ padding: 0 32rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.input-icon-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 100rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.input-placeholder {
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-row {
|
|
|
+ margin: 32rpx 0 48rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.text-link {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #ff9500;
|
|
|
+}
|
|
|
+
|
|
|
+.login-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 104rpx;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fff;
|
|
|
+ background: linear-gradient(90deg, #ffd53f, #ff9500);
|
|
|
+ border: none;
|
|
|
+ border-radius: 52rpx;
|
|
|
+ letter-spacing: 4rpx;
|
|
|
+ &::after { border: none; }
|
|
|
+}
|
|
|
+
|
|
|
+.register-redirect-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 36rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.redirect-hint {
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.redirect-action {
|
|
|
+ color: #ff9500;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-left: 12rpx;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-picker {
|
|
|
+ flex: 1;
|
|
|
+ height: 100rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-value {
|
|
|
+ height: 100rpx;
|
|
|
+ line-height: 100rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-value.placeholder {
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+.textarea-group {
|
|
|
+ height: auto;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding-top: 24rpx;
|
|
|
+ padding-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.textarea-icon-wrap {
|
|
|
+ margin-top: 4rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-textarea {
|
|
|
+ flex: 1;
|
|
|
+ height: 140rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-upload-wrap {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-placeholder {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ background: #f5f6f8;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: 2rpx dashed #d9d9d9;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-image {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2rpx solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.avatar-text {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+</style>
|