Register.vue 8.9 KB

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