register.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. <template>
  2. <view class="login-page">
  3. <nav-bar title="注册" bgColor="transparent" color="#fff" :showBack="false"></nav-bar>
  4. <!-- 顶部渐变装饰区 -->
  5. <view class="hero-bg">
  6. <view class="deco-circle c1"></view>
  7. <view class="deco-circle c2"></view>
  8. <view class="deco-circle c3"></view>
  9. <!-- 返回按钮 -->
  10. <view class="back-btn" @click="onClickLeft">
  11. <uni-icons type="left" size="22" color="#fff"></uni-icons>
  12. </view>
  13. <!-- Logo 区域 -->
  14. <view class="hero-content">
  15. <text class="brand-desc">开启您的好萌友服务之旅</text>
  16. </view>
  17. </view>
  18. <!-- 表单白色卡片区 -->
  19. <view class="form-card">
  20. <!-- 头像上传 -->
  21. <view class="avatar-upload-wrap" @click="onChooseAvatar">
  22. <image v-if="avatar" :src="avatar" class="avatar-image" mode="aspectFill"></image>
  23. <view v-else class="avatar-placeholder">
  24. <uni-icons type="camera-filled" size="32" color="#ccc"></uni-icons>
  25. <text class="avatar-text">上传头像</text>
  26. </view>
  27. </view>
  28. <!-- 商户名称 -->
  29. <view class="input-group">
  30. <view class="input-icon-wrap">
  31. <uni-icons type="shop" size="18" color="#ffc837"></uni-icons>
  32. </view>
  33. <input class="custom-input" v-model="companyName" placeholder="请输入商户名称"
  34. placeholder-class="input-placeholder" />
  35. </view>
  36. <!-- 用户账号 -->
  37. <view class="input-group">
  38. <view class="input-icon-wrap">
  39. <uni-icons type="person" size="18" color="#ffc837"></uni-icons>
  40. </view>
  41. <input class="custom-input" v-model="username" placeholder="请输入用户账号"
  42. placeholder-class="input-placeholder" />
  43. </view>
  44. <!-- 姓名 -->
  45. <view class="input-group">
  46. <view class="input-icon-wrap">
  47. <uni-icons type="staff" size="18" color="#ffc837"></uni-icons>
  48. </view>
  49. <input class="custom-input" v-model="name" placeholder="请输入姓名" placeholder-class="input-placeholder" />
  50. </view>
  51. <!-- 邮箱 -->
  52. <view class="input-group">
  53. <view class="input-icon-wrap">
  54. <uni-icons type="email" size="18" color="#ffc837"></uni-icons>
  55. </view>
  56. <input class="custom-input" v-model="email" placeholder="请输入邮箱" placeholder-class="input-placeholder" />
  57. </view>
  58. <!-- 密码 -->
  59. <view class="input-group">
  60. <view class="input-icon-wrap">
  61. <uni-icons type="locked" size="18" color="#ffc837"></uni-icons>
  62. </view>
  63. <input class="custom-input" v-model="password" type="password" placeholder="请输入密码"
  64. placeholder-class="input-placeholder" />
  65. </view>
  66. <!-- 确认密码 -->
  67. <view class="input-group">
  68. <view class="input-icon-wrap">
  69. <uni-icons type="locked" size="18" color="#ffc837"></uni-icons>
  70. </view>
  71. <input class="custom-input" v-model="confirmPassword" type="password" placeholder="请再次输入密码"
  72. placeholder-class="input-placeholder" />
  73. </view>
  74. <!-- 协议勾选 -->
  75. <view class="agreement-row">
  76. <checkbox-group @change="onCheckChange">
  77. <label class="agree-label">
  78. <checkbox :checked="checked" color="#ffc837" style="transform: scale(0.7);" />
  79. <text class="agree-text">我已阅读并同意</text>
  80. <text class="text-link" @click.stop="showAgreement(2)">《隐私政策》</text>
  81. <text class="agree-text">和</text>
  82. <text class="text-link" @click.stop="showAgreement(1)">《用户服务协议》</text>
  83. </label>
  84. </checkbox-group>
  85. </view>
  86. <!-- 提交注册按钮 -->
  87. <button class="login-btn" @click="onSubmit">提交注册申请</button>
  88. <!-- 返回登录 -->
  89. <view class="register-redirect-row">
  90. <text class="redirect-hint">已有账号?</text>
  91. <text class="redirect-action" @click="goToLogin">立即登录</text>
  92. </view>
  93. </view>
  94. <!-- 自定义权限弹窗 -->
  95. <view class="permission-modal-mask" v-if="showPermissionModal" @touchmove.stop.prevent>
  96. <view class="permission-modal-content">
  97. <view class="permission-modal-header">
  98. <view class="shield-icon-box">
  99. <uni-icons type="info-filled" size="30" color="#ff9500"></uni-icons>
  100. </view>
  101. <text class="permission-modal-title">权限申请提示</text>
  102. </view>
  103. <view class="permission-modal-body">
  104. <text class="permission-desc-main">为了正常更换头像,我们需要获取您的以下权限:</text>
  105. <view class="permission-list-box">
  106. <view class="permission-item-box">
  107. <view class="icon-circle">
  108. <uni-icons type="images-filled" size="20" color="#ff9500"></uni-icons>
  109. </view>
  110. <view class="item-text-box">
  111. <text class="item-name-text">存储与相册权限</text>
  112. <text class="item-desc-text">用于从相册中选择已有照片作为头像</text>
  113. </view>
  114. </view>
  115. <view class="permission-item-box">
  116. <view class="icon-circle">
  117. <uni-icons type="camera-filled" size="20" color="#ff9500"></uni-icons>
  118. </view>
  119. <view class="item-text-box">
  120. <text class="item-name-text">相机拍照权限</text>
  121. <text class="item-desc-text">用于拍摄新照片并上传作为头像</text>
  122. </view>
  123. </view>
  124. </view>
  125. </view>
  126. <view class="permission-modal-footer">
  127. <button class="btn-decline" @click="declinePermission">暂不授权</button>
  128. <button class="btn-grant" @click="confirmPermission">同意并授权</button>
  129. </view>
  130. </view>
  131. </view>
  132. </view>
  133. </template>
  134. <script setup>
  135. import { ref } from 'vue'
  136. import navBar from '@/components/nav-bar/index.vue'
  137. import { register } from '@/api/auth'
  138. const avatar = ref('')
  139. const companyName = ref('')
  140. const username = ref('')
  141. const name = ref('')
  142. const email = ref('')
  143. const password = ref('')
  144. const confirmPassword = ref('')
  145. const checked = ref(false)
  146. const showPermissionModal = ref(false)
  147. const onChooseAvatar = () => {
  148. const hasShown = uni.getStorageSync('has_shown_avatar_permission')
  149. if (!hasShown) {
  150. showPermissionModal.value = true
  151. } else {
  152. chooseAvatarImage()
  153. }
  154. }
  155. const chooseAvatarImage = () => {
  156. uni.chooseImage({
  157. count: 1,
  158. sizeType: ['compressed'],
  159. sourceType: ['album', 'camera'],
  160. success: (res) => {
  161. avatar.value = res.tempFilePaths[0]
  162. }
  163. })
  164. }
  165. const confirmPermission = () => {
  166. showPermissionModal.value = false
  167. uni.setStorageSync('has_shown_avatar_permission', true)
  168. chooseAvatarImage()
  169. }
  170. const declinePermission = () => {
  171. showPermissionModal.value = false
  172. uni.setStorageSync('has_shown_avatar_permission', true)
  173. }
  174. const onCheckChange = () => {
  175. checked.value = !checked.value
  176. }
  177. const goToLogin = () => {
  178. uni.reLaunch({
  179. url: '/pages/login/index'
  180. })
  181. }
  182. const onClickLeft = () => {
  183. uni.navigateBack({
  184. fail: () => {
  185. goToLogin()
  186. }
  187. })
  188. }
  189. const showAgreement = (agreementId) => {
  190. const title = agreementId === 1 ? '用户服务协议' : '隐私政策'
  191. uni.navigateTo({
  192. url: `/pages/my/agreement/detail/index?id=${agreementId}&title=${encodeURIComponent(title)}`
  193. })
  194. }
  195. const onSubmit = async () => {
  196. // 1. 商户名称验证
  197. if (!companyName.value) {
  198. uni.showToast({ title: '请输入商户名称', icon: 'none' })
  199. return
  200. }
  201. if (companyName.value.length < 2 || companyName.value.length > 50) {
  202. uni.showToast({ title: '商户名称长度需在 2 到 50 个字符之间', icon: 'none' })
  203. return
  204. }
  205. // 2. 用户账号验证
  206. if (!username.value) {
  207. uni.showToast({ title: '请输入用户账号', icon: 'none' })
  208. return
  209. }
  210. const usernameRegex = /^[a-zA-Z0-9_-]{4,20}$/
  211. if (!usernameRegex.test(username.value)) {
  212. uni.showToast({ title: '账号必须为 4 到 20 位字母、数字、下划线或减号', icon: 'none' })
  213. return
  214. }
  215. // 3. 姓名验证
  216. if (!name.value) {
  217. uni.showToast({ title: '请输入姓名', icon: 'none' })
  218. return
  219. }
  220. if (name.value.length < 2 || name.value.length > 20) {
  221. uni.showToast({ title: '姓名长度需在 2 到 20 个字符之间', icon: 'none' })
  222. return
  223. }
  224. // 4. 邮箱验证
  225. if (!email.value) {
  226. uni.showToast({ title: '请输入邮箱', icon: 'none' })
  227. return
  228. }
  229. const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
  230. if (!emailRegex.test(email.value)) {
  231. uni.showToast({ title: '请输入正确的邮箱格式', icon: 'none' })
  232. return
  233. }
  234. // 5. 密码验证
  235. if (!password.value) {
  236. uni.showToast({ title: '请输入密码', icon: 'none' })
  237. return
  238. }
  239. if (password.value.length < 6 || password.value.length > 20) {
  240. uni.showToast({ title: '密码长度需在 6 到 20 个字符之间', icon: 'none' })
  241. return
  242. }
  243. // 6. 确认密码验证
  244. if (!confirmPassword.value) {
  245. uni.showToast({ title: '请再次输入密码', icon: 'none' })
  246. return
  247. }
  248. if (password.value !== confirmPassword.value) {
  249. uni.showToast({ title: '两次输入的密码不一致', icon: 'none' })
  250. return
  251. }
  252. // 7. 协议勾选验证
  253. if (!checked.value) {
  254. uni.showToast({ title: '请先阅读并勾选协议', icon: 'none' })
  255. return
  256. }
  257. try {
  258. uni.showLoading({ title: '提交申请中...' })
  259. await register({
  260. avatar: avatar.value,
  261. companyName: companyName.value,
  262. username: username.value,
  263. name: name.value,
  264. email: email.value,
  265. password: password.value,
  266. confirmPassword: confirmPassword.value
  267. })
  268. uni.hideLoading()
  269. uni.showToast({
  270. title: '提交成功',
  271. icon: 'success'
  272. })
  273. // 缓存商户名称和提交时间,供审核界面展示使用
  274. uni.setStorageSync('registered_merchant', companyName.value)
  275. uni.setStorageSync('registered_time', new Date().toLocaleString())
  276. setTimeout(() => {
  277. uni.reLaunch({
  278. url: '/pages/login/review'
  279. })
  280. }, 1000)
  281. } catch (error) {
  282. console.error('Register error:', error)
  283. uni.hideLoading()
  284. setTimeout(() => {
  285. uni.showToast({ title: typeof error === 'string' ? error : '注册提交失败', icon: 'none' })
  286. }, 100)
  287. }
  288. }
  289. </script>
  290. <style lang="scss" scoped>
  291. .login-page {
  292. min-height: 100vh;
  293. background: #f2f3f7;
  294. display: flex;
  295. flex-direction: column;
  296. }
  297. :deep(.nav-bar) {
  298. position: absolute !important;
  299. }
  300. .back-btn {
  301. position: absolute;
  302. top: calc(var(--status-bar-height, 44px) + 20rpx);
  303. left: 32rpx;
  304. width: 76rpx;
  305. height: 76rpx;
  306. border-radius: 50%;
  307. background: rgba(255, 255, 255, 0.35);
  308. display: flex;
  309. align-items: center;
  310. justify-content: center;
  311. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
  312. z-index: 1000;
  313. }
  314. .hero-bg {
  315. background: linear-gradient(150deg, #ffd53f 0%, #ff9500 100%);
  316. padding: 0 40rpx 80rpx;
  317. position: relative;
  318. overflow: hidden;
  319. min-height: 300rpx;
  320. display: flex;
  321. flex-direction: column;
  322. justify-content: flex-end;
  323. }
  324. .deco-circle {
  325. position: absolute;
  326. border-radius: 50%;
  327. background: rgba(255, 255, 255, 0.12);
  328. }
  329. .c1 {
  330. width: 400rpx;
  331. height: 400rpx;
  332. top: -160rpx;
  333. right: -120rpx;
  334. }
  335. .c2 {
  336. width: 260rpx;
  337. height: 260rpx;
  338. top: 80rpx;
  339. left: -100rpx;
  340. }
  341. .c3 {
  342. width: 160rpx;
  343. height: 160rpx;
  344. bottom: 80rpx;
  345. right: 80rpx;
  346. }
  347. .hero-content {
  348. position: relative;
  349. z-index: 2;
  350. }
  351. .brand-desc {
  352. display: block;
  353. font-size: 28rpx;
  354. color: rgba(255, 255, 255, 0.8);
  355. letter-spacing: 2rpx;
  356. }
  357. .form-card {
  358. background: #fff;
  359. border-radius: 56rpx 56rpx 0 0;
  360. margin-top: -60rpx;
  361. padding: 60rpx 48rpx 60rpx;
  362. flex: 1;
  363. position: relative;
  364. z-index: 10;
  365. }
  366. .input-group {
  367. display: flex;
  368. align-items: center;
  369. background: #f5f6f8;
  370. border-radius: 28rpx;
  371. padding: 0 32rpx;
  372. height: 100rpx;
  373. margin-bottom: 32rpx;
  374. }
  375. .input-icon-wrap {
  376. display: flex;
  377. align-items: center;
  378. justify-content: center;
  379. margin-right: 20rpx;
  380. }
  381. .custom-input {
  382. flex: 1;
  383. height: 100rpx;
  384. font-size: 28rpx;
  385. color: #333;
  386. }
  387. .input-placeholder {
  388. color: #bbb;
  389. }
  390. .agreement-row {
  391. margin: 32rpx 0 48rpx 0;
  392. }
  393. .agree-label {
  394. display: flex;
  395. align-items: center;
  396. flex-wrap: wrap;
  397. }
  398. .agree-text {
  399. font-size: 24rpx;
  400. color: #999;
  401. }
  402. .text-link {
  403. font-size: 24rpx;
  404. color: #ff9500;
  405. }
  406. .login-btn {
  407. width: 100%;
  408. height: 104rpx;
  409. font-size: 34rpx;
  410. font-weight: 700;
  411. color: #fff;
  412. background: linear-gradient(90deg, #ffd53f, #ff9500);
  413. border: none;
  414. border-radius: 52rpx;
  415. letter-spacing: 4rpx;
  416. &::after {
  417. border: none;
  418. }
  419. }
  420. .register-redirect-row {
  421. display: flex;
  422. justify-content: center;
  423. align-items: center;
  424. margin-top: 36rpx;
  425. font-size: 26rpx;
  426. }
  427. .redirect-hint {
  428. color: #999;
  429. }
  430. .redirect-action {
  431. color: #ff9500;
  432. font-weight: bold;
  433. margin-left: 12rpx;
  434. text-decoration: underline;
  435. }
  436. .custom-picker {
  437. flex: 1;
  438. height: 100rpx;
  439. }
  440. .picker-value {
  441. height: 100rpx;
  442. line-height: 100rpx;
  443. font-size: 28rpx;
  444. color: #333;
  445. text-align: left;
  446. }
  447. .picker-value.placeholder {
  448. color: #bbb;
  449. }
  450. .textarea-group {
  451. height: auto;
  452. align-items: flex-start;
  453. padding-top: 24rpx;
  454. padding-bottom: 24rpx;
  455. }
  456. .textarea-icon-wrap {
  457. margin-top: 4rpx;
  458. }
  459. .custom-textarea {
  460. flex: 1;
  461. height: 140rpx;
  462. font-size: 28rpx;
  463. color: #333;
  464. line-height: 1.4;
  465. }
  466. .avatar-upload-wrap {
  467. display: flex;
  468. justify-content: center;
  469. margin-bottom: 40rpx;
  470. }
  471. .avatar-placeholder {
  472. width: 160rpx;
  473. height: 160rpx;
  474. background: #f5f6f8;
  475. border-radius: 50%;
  476. display: flex;
  477. flex-direction: column;
  478. align-items: center;
  479. justify-content: center;
  480. border: 2rpx dashed #d9d9d9;
  481. }
  482. .avatar-image {
  483. width: 160rpx;
  484. height: 160rpx;
  485. border-radius: 50%;
  486. border: 2rpx solid #eee;
  487. }
  488. .avatar-text {
  489. font-size: 20rpx;
  490. color: #999;
  491. margin-top: 8rpx;
  492. }
  493. /* ==================== 权限弹窗样式 ==================== */
  494. .permission-modal-mask {
  495. position: fixed;
  496. top: 0;
  497. left: 0;
  498. right: 0;
  499. bottom: 0;
  500. background: rgba(0, 0, 0, 0.6);
  501. backdrop-filter: blur(10rpx);
  502. display: flex;
  503. align-items: center;
  504. justify-content: center;
  505. z-index: 9999;
  506. padding: 50rpx;
  507. }
  508. .permission-modal-content {
  509. width: 100%;
  510. background: #ffffff;
  511. border-radius: 36rpx;
  512. display: flex;
  513. flex-direction: column;
  514. overflow: hidden;
  515. box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
  516. animation: modalFadeIn 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  517. }
  518. @keyframes modalFadeIn {
  519. from {
  520. opacity: 0;
  521. transform: scale(0.9);
  522. }
  523. to {
  524. opacity: 1;
  525. transform: scale(1);
  526. }
  527. }
  528. .permission-modal-header {
  529. display: flex;
  530. flex-direction: column;
  531. align-items: center;
  532. padding: 48rpx 40rpx 20rpx;
  533. .shield-icon-box {
  534. width: 90rpx;
  535. height: 90rpx;
  536. border-radius: 45rpx;
  537. background: #fff8eb;
  538. display: flex;
  539. align-items: center;
  540. justify-content: center;
  541. margin-bottom: 16rpx;
  542. }
  543. .permission-modal-title {
  544. font-size: 32rpx;
  545. font-weight: 800;
  546. color: #1a1a1a;
  547. }
  548. }
  549. .permission-modal-body {
  550. padding: 0 40rpx 30rpx;
  551. .permission-desc-main {
  552. font-size: 26rpx;
  553. color: #666;
  554. line-height: 1.5;
  555. display: block;
  556. text-align: center;
  557. margin-bottom: 24rpx;
  558. }
  559. }
  560. .permission-list-box {
  561. display: flex;
  562. flex-direction: column;
  563. gap: 16rpx;
  564. }
  565. .permission-item-box {
  566. display: flex;
  567. align-items: center;
  568. background: #fdfdfd;
  569. border: 2rpx solid #f6f7f9;
  570. border-radius: 20rpx;
  571. padding: 20rpx;
  572. .icon-circle {
  573. width: 64rpx;
  574. height: 64rpx;
  575. border-radius: 32rpx;
  576. background: #fff5e6;
  577. display: flex;
  578. align-items: center;
  579. justify-content: center;
  580. margin-right: 16rpx;
  581. flex-shrink: 0;
  582. }
  583. .item-text-box {
  584. flex: 1;
  585. .item-name-text {
  586. font-size: 26rpx;
  587. font-weight: 700;
  588. color: #333;
  589. display: block;
  590. margin-bottom: 4rpx;
  591. text-align: left;
  592. }
  593. .item-desc-text {
  594. font-size: 22rpx;
  595. color: #999;
  596. display: block;
  597. text-align: left;
  598. }
  599. }
  600. }
  601. .permission-modal-footer {
  602. padding: 24rpx 40rpx 40rpx;
  603. display: flex;
  604. gap: 20rpx;
  605. border-top: 2rpx solid #f5f6f8;
  606. background: #ffffff;
  607. }
  608. .btn-decline {
  609. flex: 1;
  610. height: 80rpx;
  611. line-height: 80rpx;
  612. font-size: 26rpx;
  613. color: #666;
  614. background: #f5f6f8;
  615. border-radius: 40rpx;
  616. border: none;
  617. font-weight: 600;
  618. text-align: center;
  619. &::after { border: none; }
  620. }
  621. .btn-grant {
  622. flex: 1.2;
  623. height: 80rpx;
  624. line-height: 80rpx;
  625. font-size: 26rpx;
  626. color: #fff;
  627. background: linear-gradient(90deg, #ffd53f, #ff9500);
  628. border-radius: 40rpx;
  629. border: none;
  630. font-weight: 700;
  631. box-shadow: 0 6rpx 16rpx rgba(255, 149, 0, 0.2);
  632. text-align: center;
  633. &::after { border: none; }
  634. }
  635. </style>