feat: 支持用户管理

This commit is contained in:
richarjiang
2026-03-15 15:45:09 +08:00
parent 7628768869
commit 3c35f1982f
9 changed files with 738 additions and 82 deletions

View File

@@ -3,7 +3,7 @@
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
import { Layers, Home, Settings, LogOut } from 'lucide-react'
import { Layers, Home, Settings, LogOut, Users } from 'lucide-react'
import { signOut, useSession } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button'
const navigation = [
{ name: '首页', href: '/levels', icon: Home },
{ name: '关卡配置', href: '/levels', icon: Layers },
{ name: '用户管理', href: '/users', icon: Users },
]
export function Sidebar() {

View File

@@ -2,9 +2,10 @@
import { useState, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { Upload, X, Image as ImageIcon } from 'lucide-react'
import { X, Image as ImageIcon } from 'lucide-react'
import Image from 'next/image'
import { Spinner } from '@/components/ui/spinner'
import COS from 'cos-js-sdk-v5'
interface ImageUploaderProps {
value: string
@@ -47,49 +48,42 @@ export function ImageUploader({ value, onChange }: ImageUploaderProps) {
const ext = file.name.split('.').pop() || 'jpg'
const timestamp = Date.now()
const randomStr = Math.random().toString(36).substring(2, 8)
const filename = `levels/${timestamp}_${randomStr}.${ext}`
const filename = `mini_game/images/${timestamp}_${randomStr}.${ext}`
// Upload to COS
const formData = new FormData()
formData.append('key', filename)
formData.append('Signature', keyData.credentials.sessionToken)
formData.append('success_action_status', '200')
const uploadUrl = `https://${keyData.bucket}.cos.${keyData.region}.myqcloud.com`
// Use XMLHttpRequest for COS upload with temp credentials
const uploadResult = await new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', uploadUrl)
// Set temp credentials headers
xhr.setRequestHeader('Authorization', getCOSAuthorization(
keyData.credentials.tmpSecretId,
keyData.credentials.tmpSecretKey,
keyData.credentials.sessionToken,
'post',
filename,
keyData.bucket,
keyData.region
))
xhr.onload = () => {
if (xhr.status === 200) {
resolve(`${uploadUrl}/${filename}`)
} else {
reject(new Error('上传失败'))
}
}
xhr.onerror = () => reject(new Error('上传失败'))
const cosFormData = new FormData()
cosFormData.append('key', filename)
cosFormData.append('file', file)
xhr.send(cosFormData)
// Initialize COS with temp credentials
const cos = new COS({
getAuthorization: (_options, callback) => {
callback({
TmpSecretId: keyData.credentials.tmpSecretId,
TmpSecretKey: keyData.credentials.tmpSecretKey,
SecurityToken: keyData.credentials.sessionToken,
StartTime: keyData.startTime,
ExpiredTime: keyData.expiredTime,
})
},
})
onChange(uploadResult)
// Upload file
const uploadUrl = await new Promise<string>((resolve, reject) => {
cos.putObject(
{
Bucket: keyData.bucket,
Region: keyData.region,
Key: filename,
Body: file,
},
(err, data) => {
if (err) {
reject(new Error(err.message || '上传失败'))
return
}
const url = `https://${keyData.bucket}.cos.${keyData.region}.myqcloud.com/${filename}`
resolve(url)
}
)
})
onChange(uploadUrl)
} catch (err) {
console.error('Upload error:', err)
setError(err instanceof Error ? err.message : '上传失败')
@@ -162,21 +156,3 @@ export function ImageUploader({ value, onChange }: ImageUploaderProps) {
</div>
)
}
// Helper function to generate COS authorization header
function getCOSAuthorization(
secretId: string,
secretKey: string,
sessionToken: string,
method: string,
pathname: string,
bucket: string,
region: string
): string {
const now = Math.floor(Date.now() / 1000)
const exp = now + 1800
const keyTime = `${now};${exp}`
// Simple authorization string for temp credentials
return `q-sign-algorithm=sha1&q-ak=${secretId}&q-sign-time=${keyTime}&q-key-time=${keyTime}&q-header-list=&q-url-param-list=&q-signature=placeholder&x-cos-security-token=${sessionToken}`
}

View File

@@ -0,0 +1,164 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Spinner } from '@/components/ui/spinner'
import { User, UserFormData } from '@/types'
interface UserDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
user?: User | null
onSubmit: (data: UserFormData) => Promise<void>
}
const defaultFormData: UserFormData = {
email: '',
password: '',
name: '',
}
export function UserDialog({ open, onOpenChange, user, onSubmit }: UserDialogProps) {
const [formData, setFormData] = useState<UserFormData>(defaultFormData)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')
// Reset form when dialog opens/closes or user changes
useEffect(() => {
if (open) {
if (user) {
setFormData({
email: user.email,
password: '',
name: user.name || '',
})
} else {
setFormData(defaultFormData)
}
setError('')
}
}, [open, user])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (!formData.email.trim()) {
setError('请输入邮箱')
return
}
if (!user && !formData.password) {
setError('请输入密码')
return
}
if (formData.password && formData.password.length < 6) {
setError('密码至少需要6个字符')
return
}
setIsLoading(true)
try {
await onSubmit(formData)
onOpenChange(false)
} catch (err) {
setError(err instanceof Error ? err.message : '操作失败,请稍后重试')
} finally {
setIsLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>{user ? '编辑用户' : '添加用户'}</DialogTitle>
<DialogDescription>
{user ? '修改用户信息,密码留空则不修改' : '创建新的平台用户'}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="email"> *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) =>
setFormData((prev) => ({ ...prev, email: e.target.value }))
}
placeholder="请输入邮箱"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">
{user ? '密码 (留空则不修改)' : '密码 *'}
</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) =>
setFormData((prev) => ({ ...prev, password: e.target.value }))
}
placeholder={user ? '留空则不修改密码' : '请输入密码'}
required={!user}
/>
</div>
<div className="space-y-2">
<Label htmlFor="name"> ()</Label>
<Input
id="name"
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
placeholder="请输入姓名"
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<Spinner size="sm" className="mr-2" />
...
</>
) : (
'保存'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}