AuthPage.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { FormEvent, useState } from 'react'
  2. import { Navigate, useSearchParams } from 'react-router-dom'
  3. import { ArrowRight, LockKeyhole, Phone } from 'lucide-react'
  4. import { Brand } from '../components/Brand'
  5. import { Footer } from '../components/Footer'
  6. import { useAuth } from '../AuthContext'
  7. export function AuthPage() {
  8. const [searchParams] = useSearchParams()
  9. const { user, authenticate } = useAuth()
  10. const [mode, setMode] = useState<'login' | 'register'>(() => searchParams.get('mode') === 'register' ? 'register' : 'login')
  11. const [busy, setBusy] = useState(false)
  12. const [error, setError] = useState('')
  13. if (user) {
  14. return <Navigate to={user.role === 'ADMIN' ? '/admin' : '/'} replace />
  15. }
  16. async function submit(e: FormEvent<HTMLFormElement>) {
  17. e.preventDefault()
  18. const f = new FormData(e.currentTarget)
  19. const password = String(f.get('password'))
  20. if (mode === 'register' && password !== String(f.get('confirmPassword'))) {
  21. setError('两次输入的密码不一致')
  22. return
  23. }
  24. setBusy(true)
  25. setError('')
  26. try {
  27. await authenticate(mode, String(f.get('phone')), password)
  28. } catch (x) {
  29. setError(x instanceof Error ? x.message : '操作失败')
  30. } finally {
  31. setBusy(false)
  32. }
  33. }
  34. return (
  35. <div className="auth-wrapper">
  36. <div className="auth-page">
  37. <section className="auth-brand">
  38. <Brand />
  39. <div>
  40. <p className="">智能课程工坊</p>
  41. <h1>
  42. 让每一门好课,
  43. <br />
  44. <em>留下清晰的星痕。</em>
  45. </h1>
  46. <p>归集课程资产与讲师信息,让课程制作更简单。</p>
  47. </div>
  48. </section>
  49. <form className="auth-card" onSubmit={submit}>
  50. <h2>{mode === 'login' ? '登录星痕' : '创建账号'}</h2>
  51. <p>{mode === 'login' ? '使用手机号和密码继续' : '注册后即可开始'}</p>
  52. <label>
  53. 手机号
  54. <div className="input-with-icon">
  55. <Phone />
  56. <input
  57. name="phone"
  58. inputMode="numeric"
  59. required
  60. pattern="1[0-9]{10}"
  61. placeholder="请输入 11 位手机号"
  62. />
  63. </div>
  64. </label>
  65. <label>
  66. 密码
  67. <div className="input-with-icon">
  68. <LockKeyhole />
  69. <input
  70. name="password"
  71. type="password"
  72. autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
  73. required
  74. minLength={6}
  75. placeholder="至少 6 位密码"
  76. />
  77. </div>
  78. </label>
  79. {mode === 'register' && (
  80. <label>
  81. 确认密码
  82. <div className="input-with-icon">
  83. <LockKeyhole />
  84. <input
  85. name="confirmPassword"
  86. type="password"
  87. autoComplete="new-password"
  88. required
  89. minLength={6}
  90. placeholder="请再次输入密码"
  91. />
  92. </div>
  93. </label>
  94. )}
  95. {error && <div className="notice notice-error">{error}</div>}
  96. <button className="primary-button full-button" disabled={busy}>
  97. {busy ? '请稍候…' : mode === 'login' ? '登录' : '注册'}
  98. <ArrowRight size={17} />
  99. </button>
  100. <button
  101. type="button"
  102. className="auth-switch"
  103. onClick={() => {
  104. setMode(mode === 'login' ? 'register' : 'login')
  105. setError('')
  106. }}
  107. >
  108. {mode === 'login' ? '还没有账号?立即注册' : '已有账号?返回登录'}
  109. </button>
  110. </form>
  111. </div>
  112. <Footer />
  113. </div>
  114. )
  115. }