6.6 KiB
Registration Page Improvement Design
Date: 2026-05-15 Branch: feat/registration-page Status: draft
Scope
Full-stack improvements to the registration page covering UI/UX, engineering quality, and visual polish. The existing register.vue (146 lines) works correctly but has accumulated technical debt and UX gaps.
Architecture: Component Decomposition (Approach B)
Split the monolithic register.vue into focused components with clear data flow.
File Structure
app/pages/register.vue # Page shell (~60 lines)
app/components/register/
RegisterForm.vue # Main form with submission logic (~120 lines)
PasswordInput.vue # Password field with show/hide toggle (~40 lines)
PasswordStrength.vue # Real-time strength indicator (~35 lines)
CaptchaField.vue # Captcha SVG + refresh + input (~50 lines)
packages/shared/auth-schema.ts # Shared Zod schema (client + server)
Data Flow
register.vue
│ Owns: captchaToken, captchaSvg, toast
│ Provides: fetchCaptcha()
│
└─ RegisterForm
│ Props: captchaSvg, captchaToken
│ Emits: refresh-captcha, success, error
│ Uses: useHttpFetch → POST /api/auth/register
│ Internal: loading state
│
├─ PasswordInput (x2: password + confirmPassword)
│ Props: modelValue, label, placeholder, disabled
│ Emits: update:modelValue
│ Internal: showPassword toggle
│
├─ PasswordStrength
│ Props: password (computed from form values)
│ Output: score 0-4, label text, color-coded bar
│
└─ CaptchaField
Props: svg (v-html), loading
Emits: refresh
Component Specifications
PasswordInput
| Prop | Type | Default | Purpose |
|---|---|---|---|
| modelValue | string | required | v-model binding |
| label | string | "密码" | Field label |
| placeholder | string | "请输入密码" | Placeholder |
| disabled | boolean | false | Disable during submission |
| Emit | Payload | Purpose |
|---|---|---|
| update:modelValue | string | v-model update |
States: default, showing password, focused (ring), error (red border + message), disabled.
PasswordStrength
| Prop | Type | Purpose |
|---|---|---|
| password | string | Password to evaluate |
Scoring algorithm:
- Empty: Nothing rendered
- Weak (1): Only digits or only letters, < 8 chars — red 1/4 bar
- Fair (2): Letters + digits, >= 8 chars — amber 2/4 bar
- Strong (3): Upper + lower + digits, >= 10 chars — green 3/4 bar
- Very Strong (4): Upper + lower + digits + special chars, >= 12 chars — emerald 4/4 bar
CaptchaField
| Prop | Type | Purpose |
|---|---|---|
| svg | string | SVG markup rendered via v-html |
| loading | boolean | Show spinner, disable input during refresh |
| Emit | Payload | Purpose |
|---|---|---|
| refresh | none | Request new captcha |
States: default (SVG + input + refresh button), refreshing (spinner + dimmed), load error (error message + retry link).
RegisterForm Lifecycle
- Idle: Form editable, submit button enabled
- Validating: Client-side Zod via UForm, field-level errors
- Submitting: Button shows loading spinner + "注册中...", all inputs/buttons disabled
- Failure: Field-level errors (via setFieldError) or generic toast
- Success: Card content replaced with success state (check icon + "注册成功" + "去登录" link)
Schema Deduplication
Extract the Zod schema from both register.vue (client inline) and server/utils/auth/validation.ts into a single shared module:
packages/shared/auth-schema.ts
exports: registerSchema
used by: client (UForm validation), server (safeParse)
HTTP Client Consistency
Replace raw $fetch in register.vue with useHttpFetch from app/utils/http/factory.ts, matching the pattern used elsewhere in the app. The useHttpFetch composable auto-unwraps the { code, message, data } envelope.
Error Handling
Response Format Extension
Existing envelope unchanged. Add optional field in data for field-level errors:
// Username conflict → field-level
{ "code": 1, "message": "用户名已存在", "data": { "field": "username" } }
// Captcha wrong → field-level
{ "code": 1, "message": "验证码错误", "data": { "field": "captchaText" } }
// Captcha expired → field-level
{ "code": 1, "message": "验证码已过期", "data": { "field": "captchaText" } }
// Unknown → generic toast
{ "code": 1, "message": "服务器内部错误", "data": null }
Error Layers
| Layer | Scenario | Handling | UI |
|---|---|---|---|
| Client validation | Format, length, mismatch | UForm + Zod, no request | Field-level red text |
| Server business | Duplicate username, wrong captcha | API returns field code | setFieldError or toast |
| Network | Timeout, offline, 500 | try/catch on useHttpFetch | Toast "网络异常,请稍后重试" |
| Mount failure | Captcha load fails | try/catch in onMounted | "加载失败,点此重试" in captcha area |
Success Flow
After successful registration, instead of navigating to /login?registered=1 (which does not exist yet), show an inline success state within the card:
- Green check icon
- "注册成功" heading
- Link/button: "去登录"
The existing redirect behavior is kept as a fallback once the login page exists.
Testing Strategy
Unit Tests (Vitest)
auth-schema.ts: Zod validation pass/fail for various inputsPasswordStrength: Scoring logic for each strength levelserver/utils/auth/validation.ts: Server-side schema validation
Component Tests (@nuxt/test-utils + @vue/test-utils)
PasswordInput: Show/hide toggle, v-model binding, disabled statePasswordStrength: Renders correct color and label for each password inputCaptchaField: SVG rendering, refresh emit, loading stateRegisterForm: Form validation, submission flow, disabled state during loading
Visual Polish
- Optimize card vertical centering and form field spacing
- Softer card shadow and border radius
- Add "已有账号?去登录" + home page links below the form
- Captcha refresh button shows rotation animation during loading
What's NOT Included
- Login page (separate feature)
- Session/JWT creation on registration (separate feature)
- Email verification (separate feature)
- Social login (separate feature)
- Rate limiting (separate feature, needs infrastructure decisions)
- Captcha storage migration from in-memory Map (low traffic, Map is sufficient for now)