@@ -15,7 +35,58 @@ export function LoginPage() {
-
+
+
+ 账号密码
+ GitHub
+
+
+
+
+
+
+
+
+ 使用 GitHub 登录时,认证完成后会自动返回当前站点。
+
+
+
+
diff --git a/web/src/pages/register.tsx b/web/src/pages/register.tsx
new file mode 100644
index 00000000..ec881f15
--- /dev/null
+++ b/web/src/pages/register.tsx
@@ -0,0 +1,83 @@
+import { Link } from '@tanstack/react-router'
+import { useState } from 'react'
+import { useLocalRegister } from '@/features/auth/use-local-auth'
+import { Button } from '@/shared/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
+import { Input } from '@/shared/ui/input'
+
+export function RegisterPage() {
+ const registerMutation = useLocalRegister()
+ const [username, setUsername] = useState('')
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+
+ async function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+ try {
+ await registerMutation.mutateAsync({ username, email, password })
+ window.location.href = '/dashboard'
+ } catch {
+ // mutation state drives the error UI
+ }
+ }
+
+ return (
+
+
+
+ 创建账号
+ 注册后会自动建立本地会话,可继续进入 Dashboard。
+
+
+
+
+
+
+ )
+}
diff --git a/web/src/pages/settings/accounts.tsx b/web/src/pages/settings/accounts.tsx
new file mode 100644
index 00000000..d143fa32
--- /dev/null
+++ b/web/src/pages/settings/accounts.tsx
@@ -0,0 +1,100 @@
+import { useState } from 'react'
+import { useInitiateAccountMerge, useVerifyAccountMerge } from '@/features/auth/use-account-merge'
+import { Button } from '@/shared/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
+import { Input } from '@/shared/ui/input'
+
+export function AccountSettingsPage() {
+ const [secondaryIdentifier, setSecondaryIdentifier] = useState('')
+ const [mergeRequestId, setMergeRequestId] = useState('')
+ const [verificationToken, setVerificationToken] = useState('')
+ const [statusMessage, setStatusMessage] = useState('')
+
+ const initiateMutation = useInitiateAccountMerge()
+ const verifyMutation = useVerifyAccountMerge()
+
+ async function handleInitiate(event: React.FormEvent) {
+ event.preventDefault()
+ setStatusMessage('')
+ try {
+ const result = await initiateMutation.mutateAsync({ secondaryIdentifier })
+ setMergeRequestId(String(result.mergeRequestId))
+ setVerificationToken(result.verificationToken)
+ setStatusMessage(`已创建合并请求,secondary=${result.secondaryUserId}`)
+ } catch (error) {
+ setStatusMessage(error instanceof Error ? error.message : '发起合并失败')
+ }
+ }
+
+ async function handleVerify(event: React.FormEvent) {
+ event.preventDefault()
+ setStatusMessage('')
+ try {
+ await verifyMutation.mutateAsync({
+ mergeRequestId: Number(mergeRequestId),
+ verificationToken,
+ })
+ setStatusMessage('账号合并已完成')
+ } catch (error) {
+ setStatusMessage(error instanceof Error ? error.message : '验证合并失败')
+ }
+ }
+
+ return (
+
+
+
+ 发起账号合并
+ 输入 secondary 账号标识。支持本地用户名,或 `provider:subject` 格式的外部身份。
+
+
+
+
+
+
+
+
+ 验证并完成合并
+ 发起后会返回一次性 token;在当前阶段直接复制该 token 完成验证。
+
+
+
+ {statusMessage ? {statusMessage}
: null}
+
+
+
+ )
+}
diff --git a/web/src/pages/settings/security.tsx b/web/src/pages/settings/security.tsx
new file mode 100644
index 00000000..b7267280
--- /dev/null
+++ b/web/src/pages/settings/security.tsx
@@ -0,0 +1,70 @@
+import { useState } from 'react'
+import { authApi } from '@/api/client'
+import { Button } from '@/shared/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
+import { Input } from '@/shared/ui/input'
+
+export function SecuritySettingsPage() {
+ const [currentPassword, setCurrentPassword] = useState('')
+ const [newPassword, setNewPassword] = useState('')
+ const [statusMessage, setStatusMessage] = useState('')
+ const [errorMessage, setErrorMessage] = useState('')
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ async function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+ setStatusMessage('')
+ setErrorMessage('')
+ setIsSubmitting(true)
+ try {
+ await authApi.changePassword({ currentPassword, newPassword })
+ setStatusMessage('密码修改成功')
+ setCurrentPassword('')
+ setNewPassword('')
+ } catch (error) {
+ setErrorMessage(error instanceof Error ? error.message : '修改密码失败')
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+
+
+
+ 安全设置
+ 已启用本地账号密码登录时,可以在这里更新密码。
+
+
+
+
+
+
+ )
+}