Register.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <template>
  2. <div class="client-register">
  3. <div class="client-register__panel">
  4. <div class="client-register__header">
  5. <div>
  6. <h2>注册账户</h2>
  7. <p>用邮箱完成注册即可开始使用</p>
  8. </div>
  9. <div class="client-register__header-link">
  10. 已有账号?<router-link to="/user/login">立即登录</router-link>
  11. </div>
  12. </div>
  13. <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-position="top"
  14. class="client-register__form">
  15. <el-form-item label="联系人姓名" prop="name">
  16. <el-input v-model.trim="registerForm.name" placeholder="请输入联系人姓名" clearable />
  17. </el-form-item>
  18. <el-form-item label="邮箱" prop="email">
  19. <el-input v-model.trim="registerForm.email" placeholder="用于接收验证码及通知" clearable />
  20. </el-form-item>
  21. <el-form-item label="邮箱验证码" prop="emailCode">
  22. <div class="client-register__code-row">
  23. <el-input class="client-register__code-input" v-model.trim="registerForm.emailCode" placeholder="请输入6位验证码"
  24. maxlength="6" />
  25. <el-button class="client-register__code-btn" type="primary" :loading="sendingCode" :disabled="codeDisabled"
  26. @click="handleSendEmailCode">
  27. {{ codeButtonText }}
  28. </el-button>
  29. </div>
  30. </el-form-item>
  31. <el-form-item label="登录密码" prop="password">
  32. <el-input v-model="registerForm.password" type="password" placeholder="至少设置6位密码,包含字母或数字" show-password />
  33. </el-form-item>
  34. <el-form-item label="确认密码" prop="confirmPassword">
  35. <el-input v-model="registerForm.confirmPassword" type="password" placeholder="再次输入密码" show-password />
  36. </el-form-item>
  37. <el-form-item label="邀请码(选填)" prop="inviteCode">
  38. <el-input :disabled="userRegister" v-model.trim="registerForm.inviteCode" placeholder="没有可留空" clearable />
  39. </el-form-item>
  40. <el-form-item>
  41. <el-button type="primary" class="client-register__submit" :loading="submitting" @click="handleRegister">
  42. 注册并登录
  43. </el-button>
  44. </el-form-item>
  45. <p class="client-register__agreement">
  46. @2019-2026 成都聚德鼎盛文化传播有限公司 蜀ICP备19030658号-1
  47. </p>
  48. </el-form>
  49. </div>
  50. </div>
  51. </template>
  52. <script setup>
  53. import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
  54. import { useRouter, useRoute } from 'vue-router'
  55. import { ElMessage } from 'element-plus'
  56. import { checkIn } from '/@/api/frontend/user/index'
  57. import { sendEms } from '/@/api/common'
  58. import { uuid } from '/@/utils/random'
  59. import clickCaptcha from '/@/components/clickCaptcha'
  60. import { useUserInfo } from '/@/stores/userInfo'
  61. defineOptions({ name: 'ClientRegister' })
  62. const router = useRouter()
  63. const route = useRoute()
  64. const userInfo = useUserInfo()
  65. // 邀请码开关
  66. const userRegister = ref(false)
  67. const registerFormRef = ref(null)
  68. const registerForm = reactive({
  69. name: '',
  70. email: '',
  71. emailCode: '',
  72. password: '',
  73. confirmPassword: '',
  74. inviteCode: '',
  75. captchaId: uuid(),
  76. captchaInfo: ''
  77. })
  78. const validateConfirmPassword = (rule, value, callback) => {
  79. if (!value) {
  80. callback(new Error('请再次输入密码'))
  81. } else if (value !== registerForm.password) {
  82. callback(new Error('两次输入的密码不一致'))
  83. } else {
  84. callback()
  85. }
  86. }
  87. const registerRules = reactive({
  88. name: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }],
  89. email: [
  90. { required: true, message: '请输入企业邮箱', trigger: 'blur' },
  91. { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
  92. ],
  93. emailCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
  94. password: [
  95. { required: true, message: '请输入密码', trigger: 'blur' },
  96. { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
  97. ],
  98. confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
  99. inviteCode: []
  100. })
  101. const submitting = ref(false)
  102. const sendingCode = ref(false)
  103. const countdown = ref(0)
  104. const countdownTimer = ref(null)
  105. const codeDisabled = computed(() => sendingCode.value || countdown.value > 0)
  106. const codeButtonText = computed(() => {
  107. if (sendingCode.value) return '发送中...'
  108. return countdown.value > 0 ? `${countdown.value}s 后重试` : '发送验证码'
  109. })
  110. const clearCountdown = () => {
  111. if (countdownTimer.value) {
  112. clearInterval(countdownTimer.value)
  113. countdownTimer.value = null
  114. }
  115. }
  116. const startCountdown = seconds => {
  117. countdown.value = seconds
  118. clearCountdown()
  119. countdownTimer.value = setInterval(() => {
  120. if (countdown.value <= 1) {
  121. clearCountdown()
  122. countdown.value = 0
  123. } else {
  124. countdown.value -= 1
  125. }
  126. }, 1000)
  127. }
  128. const handleRegister = () => {
  129. if (!registerFormRef.value) return
  130. registerFormRef.value.validate(async valid => {
  131. if (!valid) return
  132. submitting.value = true
  133. try {
  134. const payload = {
  135. tab: 'register',
  136. registerType: 'email',
  137. name: registerForm.name.trim(),
  138. email: registerForm.email.trim(),
  139. username: registerForm.name.trim(),
  140. password: registerForm.password,
  141. captcha: registerForm.emailCode.trim(),
  142. captchaId: registerForm.captchaId,
  143. captchaInfo: registerForm.captchaInfo,
  144. inviteCode: registerForm.inviteCode.trim() || undefined
  145. }
  146. const res = await checkIn('post', payload)
  147. // 注册成功后直接填充用户信息并跳转
  148. if (res?.data?.userInfo) {
  149. userInfo.dataFill(res.data.userInfo, false)
  150. }
  151. if (res?.data?.routePath) {
  152. router.push({ path: res.data.routePath })
  153. } else {
  154. router.push({ path: '/user/home' })
  155. }
  156. } catch (error) {
  157. ElMessage.error(error?.message || '注册失败,请稍后重试')
  158. // 注册失败后重置验证码ID以防止重复验证
  159. registerForm.captchaId = uuid()
  160. } finally {
  161. submitting.value = false
  162. }
  163. })
  164. }
  165. const handleSendEmailCode = () => {
  166. if (!registerFormRef.value) return
  167. if (codeDisabled.value) return
  168. if (!registerForm.email.trim()) {
  169. ElMessage.warning('请输入邮箱地址')
  170. return
  171. }
  172. registerFormRef.value.validateField('email').then((valid) => {
  173. if (!valid) {
  174. // 验证失败,Element Plus 会自动显示错误信息
  175. return
  176. }
  177. // 邮箱验证通过,发送验证码前先进行点选验证码验证
  178. clickCaptcha(registerForm.captchaId, async (captchaInfo) => {
  179. sendingCode.value = true
  180. registerForm.captchaInfo = captchaInfo
  181. try {
  182. await sendEms(registerForm.email.trim(), 'user_register', {
  183. captchaInfo,
  184. captchaId: registerForm.captchaId
  185. })
  186. ElMessage.success('验证码发送成功,请查收邮箱')
  187. startCountdown(60)
  188. } catch (err) {
  189. ElMessage.error(err?.message || '验证码发送失败')
  190. // 失败时刷新点击验证码ID
  191. registerForm.captchaId = uuid()
  192. } finally {
  193. sendingCode.value = false
  194. }
  195. })
  196. })
  197. }
  198. // 组件挂载时检查 URL 中的邀请码参数
  199. onMounted(() => {
  200. const inviteCode = route.query.invite
  201. if (inviteCode && typeof inviteCode === 'string') {
  202. registerForm.inviteCode = inviteCode.trim()
  203. userRegister.value = true
  204. }
  205. })
  206. onBeforeUnmount(() => {
  207. clearCountdown()
  208. })
  209. </script>
  210. <style scoped>
  211. .client-register {
  212. min-height: 100vh;
  213. display: flex;
  214. align-items: center;
  215. justify-content: center;
  216. padding: 48px 16px;
  217. background: url('/@/assets/images/login_bg.jpg') center center / cover no-repeat;
  218. }
  219. .client-register__panel {
  220. width: 520px;
  221. max-width: 100%;
  222. padding: 28px 36px 24px;
  223. background: #fff;
  224. border-radius: 16px;
  225. box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
  226. }
  227. .client-register__header {
  228. display: flex;
  229. justify-content: space-between;
  230. align-items: center;
  231. margin-bottom: 20px;
  232. }
  233. .client-register__header h2 {
  234. margin: 0;
  235. font-size: 22px;
  236. color: #303133;
  237. }
  238. .client-register__header p {
  239. margin: 6px 0 0;
  240. color: #909399;
  241. }
  242. .client-register__header-link {
  243. font-size: 14px;
  244. color: #606266;
  245. }
  246. .client-register__header-link a {
  247. color: #409eff;
  248. margin-left: 4px;
  249. }
  250. .client-register__form ::v-deep .el-form-item__label {
  251. font-weight: 500;
  252. color: #303133;
  253. }
  254. .client-register__form :deep(.el-form-item) {
  255. margin-bottom: 18px;
  256. }
  257. .client-register__form :deep(.el-input) {
  258. width: 100%;
  259. }
  260. .client-register__code-row {
  261. display: flex;
  262. align-items: stretch;
  263. width: 100%;
  264. }
  265. .client-register__code-input {
  266. flex: 1;
  267. }
  268. .client-register__code-btn {
  269. white-space: nowrap;
  270. margin-left: -1px;
  271. min-width: 128px;
  272. border-radius: 0 8px 8px 0;
  273. }
  274. .client-register__code-input :deep(.el-input__wrapper) {
  275. border-radius: 8px 0 0 8px;
  276. }
  277. .client-register__submit {
  278. width: 100%;
  279. height: 44px;
  280. font-size: 15px;
  281. }
  282. .client-register__agreement {
  283. margin: -8px 0 0;
  284. font-size: 12px;
  285. color: #909399;
  286. text-align: center;
  287. }
  288. .client-register__agreement a {
  289. color: #409eff;
  290. text-decoration: none;
  291. }
  292. @media (max-width: 600px) {
  293. .client-register {
  294. padding: 24px 12px;
  295. }
  296. .client-register__panel {
  297. padding: 24px 20px;
  298. }
  299. .client-register__header {
  300. flex-direction: column;
  301. align-items: flex-start;
  302. gap: 6px;
  303. }
  304. }
  305. </style>