小说网站前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
11 KiB

3 weeks ago
3 weeks ago
  1. <template>
  2. <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)"
  3. :title="isLogin ? '手机号登录' : '手机号注册'" width="500px" :show-close="true" :close-on-click-modal="false" center
  4. class="login-register-modal">
  5. <div class="auth-form">
  6. <div class="phone-input">
  7. <el-select v-model="countryCode" class="country-code">
  8. <el-option label="+86" value="+86" />
  9. <!-- <el-option label="+852" value="+852" />
  10. <el-option label="+853" value="+853" />
  11. <el-option label="+886" value="+886" /> -->
  12. </el-select>
  13. <el-input v-model="phone" placeholder="手机号" />
  14. </div>
  15. <div class="verification-input">
  16. <el-input v-model="verificationCode" placeholder="验证码" />
  17. <el-button type="primary" :disabled="countdownActive" @click="sendVerificationCode">
  18. {{ countdownActive ? `${countdown}秒后重发` : '发送验证码' }}
  19. </el-button>
  20. </div>
  21. <el-button type="primary" class="submit-button" @click="handleSubmit">
  22. {{ isLogin ? '登录' : '注册' }}
  23. </el-button>
  24. <!-- <div class="form-options">
  25. <span class="switch-auth" @click="switchAuthType">
  26. {{ isLogin ? '手机号注册' : '已有账号登录' }}
  27. </span>
  28. </div> -->
  29. <div class="agreement">
  30. <el-checkbox v-model="agreeTerms"></el-checkbox>
  31. <span class="agreement-text">
  32. 登录即表示您同意 <a href="javascript:void(0)">用户协议</a> <a href="javascript:void(0)">隐私政策</a>
  33. </span>
  34. </div>
  35. </div>
  36. </el-dialog>
  37. </template>
  38. <script>
  39. import { ref, computed } from 'vue';
  40. import { ElMessage } from 'element-plus';
  41. import { useMainStore } from '@/store';
  42. import { authApi } from '@/api/auth';
  43. export default {
  44. name: 'LoginRegisterModal',
  45. props: {
  46. visible: {
  47. type: Boolean,
  48. required: true
  49. },
  50. defaultType: {
  51. type: String,
  52. default: 'login',
  53. validator: (val) => ['login', 'register'].includes(val)
  54. }
  55. },
  56. emits: ['update:visible', 'login-success', 'register-success'],
  57. setup(props, { emit }) {
  58. const store = useMainStore();
  59. const authType = ref(props.defaultType);
  60. const isLogin = computed(() => authType.value === 'login');
  61. const countryCode = ref('+86');
  62. const phone = ref('');
  63. const verificationCode = ref('');
  64. const agreeTerms = ref(false);
  65. const countdown = ref(60);
  66. const countdownActive = ref(false);
  67. const validateForm = () => {
  68. if (!phone.value) {
  69. ElMessage.error('请输入手机号');
  70. return false;
  71. }
  72. if (!verificationCode.value) {
  73. ElMessage.error('请输入验证码');
  74. return false;
  75. }
  76. if (!agreeTerms.value) {
  77. ElMessage.error('请同意用户协议和隐私政策');
  78. return false;
  79. }
  80. return true;
  81. };
  82. const sendVerificationCode = async () => {
  83. if (!phone.value) {
  84. ElMessage.error('请输入手机号');
  85. return;
  86. }
  87. // 简单的手机号格式验证
  88. const phoneRegex = /^1[3-9]\d{9}$/;
  89. if (!phoneRegex.test(phone.value)) {
  90. ElMessage.error('请输入正确的手机号格式');
  91. return;
  92. }
  93. try {
  94. // 调用发送验证码接口
  95. const response = await authApi.phoneSendCode({
  96. phone: phone.value
  97. });
  98. if (response.success) {
  99. ElMessage.success(`验证码已发送至 ${countryCode.value}${phone.value}`);
  100. // 开始倒计时
  101. countdownActive.value = true;
  102. countdown.value = 60;
  103. const timer = setInterval(() => {
  104. countdown.value--;
  105. if (countdown.value <= 0) {
  106. clearInterval(timer);
  107. countdownActive.value = false;
  108. }
  109. }, 1000);
  110. } else {
  111. ElMessage.error(response.message || '发送验证码失败,请稍后重试');
  112. }
  113. } catch (error) {
  114. console.error('发送验证码失败:', error);
  115. ElMessage.error('发送验证码失败,请稍后重试');
  116. }
  117. };
  118. const handleSubmit = async () => {
  119. if (!validateForm()) return;
  120. try {
  121. if (isLogin.value) {
  122. // 登录逻辑 - 调用真实API
  123. const response = await authApi.phoneLogin({
  124. phone: phone.value,
  125. code: verificationCode.value
  126. });
  127. if (response.success && response.result) {
  128. // 使用store的login方法处理登录成功后的逻辑
  129. await store.handleLoginSuccess(response.result);
  130. emit('login-success');
  131. ElMessage.success('登录成功');
  132. } else {
  133. throw new Error(response.message || '登录失败');
  134. }
  135. } else {
  136. // 注册逻辑 - 目前使用登录接口,如果后端有单独注册接口可以替换
  137. const response = await authApi.phoneLogin({
  138. phone: phone.value,
  139. code: verificationCode.value
  140. });
  141. if (response.success && response.result) {
  142. await store.handleLoginSuccess(response.result);
  143. emit('register-success');
  144. ElMessage.success('注册成功');
  145. } else {
  146. throw new Error(response.message || '注册失败');
  147. }
  148. }
  149. // 关闭弹窗
  150. emit('update:visible', false);
  151. // 重置表单
  152. resetForm();
  153. } catch (error) {
  154. console.error('登录/注册失败:', error);
  155. ElMessage.error(error.message || '操作失败,请稍后重试');
  156. }
  157. };
  158. const switchAuthType = () => {
  159. authType.value = isLogin.value ? 'register' : 'login';
  160. };
  161. const resetForm = () => {
  162. phone.value = '';
  163. verificationCode.value = '';
  164. agreeTerms.value = false;
  165. countdownActive.value = false;
  166. countdown.value = 60;
  167. };
  168. return {
  169. isLogin,
  170. countryCode,
  171. phone,
  172. verificationCode,
  173. agreeTerms,
  174. countdown,
  175. countdownActive,
  176. sendVerificationCode,
  177. handleSubmit,
  178. switchAuthType
  179. };
  180. }
  181. };
  182. </script>
  183. <style lang="scss" scoped>
  184. @use '@/assets/styles/variables.scss' as vars;
  185. .login-register-modal {
  186. // 全局共享样式
  187. :deep(.el-input__wrapper),
  188. :deep(.el-input__inner),
  189. :deep(.el-select .el-input__wrapper),
  190. :deep(.el-button) {
  191. height: 44px !important;
  192. line-height: 44px !important;
  193. box-sizing: border-box;
  194. }
  195. :deep(.el-input__wrapper) {
  196. padding: 0 15px;
  197. }
  198. :deep(.el-input__inner) {
  199. font-size: 15px;
  200. }
  201. // 统一表单控件样式
  202. :deep(.el-select),
  203. :deep(.el-input) {
  204. --el-select-input-focus-border-color: var(--el-color-primary);
  205. }
  206. // 特别处理select下拉框,使其与输入框完全一致
  207. :deep(.el-select) {
  208. width: 100%;
  209. .el-input {
  210. width: 100%;
  211. }
  212. }
  213. // 按钮样式统一
  214. :deep(.el-button) {
  215. border: none;
  216. }
  217. :deep(.el-dialog) {
  218. border-radius: 8px;
  219. overflow: hidden;
  220. }
  221. :deep(.el-dialog__header) {
  222. padding: 24px 30px;
  223. text-align: center;
  224. border-bottom: 1px solid #f0f0f0;
  225. .el-dialog__title {
  226. font-size: 22px;
  227. font-weight: bold;
  228. color: #333;
  229. }
  230. }
  231. :deep(.el-dialog__body) {
  232. padding: 40px 50px;
  233. }
  234. .auth-form {
  235. padding: 30px 30px 50px 30px;
  236. .phone-input {
  237. display: flex;
  238. margin-bottom: 24px;
  239. .country-code {
  240. width: 100px;
  241. margin-right: 12px;
  242. flex-shrink: 0;
  243. }
  244. .el-input {
  245. flex: 1;
  246. }
  247. }
  248. .verification-input {
  249. display: flex;
  250. margin-bottom: 30px;
  251. .el-input {
  252. flex: 1;
  253. }
  254. .el-button {
  255. margin-left: 12px;
  256. width: 120px;
  257. background-color: vars.$primary-color;
  258. font-size: 15px;
  259. padding: 0 15px;
  260. color: white;
  261. flex-shrink: 0;
  262. }
  263. }
  264. .submit-button {
  265. width: 100%;
  266. height: 48px !important;
  267. line-height: 48px !important;
  268. background-color: vars.$primary-color;
  269. margin-bottom: 20px;
  270. font-size: 16px;
  271. letter-spacing: 1px;
  272. }
  273. .form-options {
  274. display: flex;
  275. justify-content: center;
  276. margin-bottom: 25px;
  277. .switch-auth {
  278. color: vars.$primary-color;
  279. cursor: pointer;
  280. font-size: 15px;
  281. &:hover {
  282. opacity: 0.8;
  283. text-decoration: underline;
  284. }
  285. }
  286. }
  287. .agreement {
  288. display: flex;
  289. align-items: center;
  290. font-size: 13px;
  291. color: #666;
  292. gap: 10px;
  293. a {
  294. color: vars.$primary-color;
  295. text-decoration: none;
  296. &:hover {
  297. text-decoration: underline;
  298. }
  299. }
  300. }
  301. }
  302. }
  303. </style>