完成admin-user-update
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
import { IsString, Length, ValidateIf } from "class-validator";
|
||||
|
||||
export class CreateDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
@ValidateIf(o => o.username !== null)
|
||||
@IsString({ message: '用户名不得为空' })
|
||||
@Length(6, 32, { message: '用户名长度只能为6~32' })
|
||||
username: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
@ValidateIf(o => o.username !== null)
|
||||
@IsString({ message: '昵称不得为空' })
|
||||
@Length(6, 30, { message: '昵称长度只能为6~30' })
|
||||
nickname: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
@ValidateIf(o => o.username !== null)
|
||||
@IsString({ message: '邮箱不得为空' })
|
||||
@Length(6, 254, { message: '邮箱长度只能为6~254' })
|
||||
email: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
@ValidateIf(o => o.username !== null)
|
||||
@IsString({ message: '手机号不得为空' })
|
||||
@Length(11, 11, { message: '手机号长度只能为11' })
|
||||
phone: string | null;
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
import { IsEmail, IsOptional, IsString, Length, Matches, ValidateIf } from "class-validator";
|
||||
|
||||
export class UpdateDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
@IsString({ message: '用户名不得为空' })
|
||||
@Length(6, 32, { message: '用户名长度只能为6~32' })
|
||||
username: string;
|
||||
|
||||
@IsString({ message: '昵称不得为空' })
|
||||
@Length(6, 30, { message: '昵称长度只能为6~30' })
|
||||
nickname: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsEmail({}, { message: '请输入有效的邮箱地址', always: false })
|
||||
@Length(6, 254, {
|
||||
message: '邮箱长度只能为6~254',
|
||||
// 仅在值不为 null 或 undefined 时验证
|
||||
always: false
|
||||
})
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsOptional() // 标记字段为可选
|
||||
@IsString({ message: '手机号不得为空', always: false })
|
||||
@Matches(/^1[3456789]\d{9}$/, {
|
||||
message: '请输入有效的手机号码',
|
||||
// 仅在值不为 null 或 undefined 时验证
|
||||
always: false
|
||||
})
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
}
|
||||
@@ -2,16 +2,18 @@ import { BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Entity()
|
||||
@Index("IDX_user_userid", ["userId"], { unique: true })
|
||||
@Index("IDX_user_username", ["username"], { unique: true })
|
||||
@Index("IDX_user_email", ["email"], { unique: true, where: "email IS NOT NULL" })
|
||||
@Index("IDX_user_phone", ["phone"], { unique: true, where: "phone IS NOT NULL" })
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column('uuid', { unique: true, default: () => 'gen_random_uuid()' })
|
||||
@Index({ unique: true })
|
||||
@Column('uuid', { default: () => 'gen_random_uuid()' })
|
||||
userId: string;
|
||||
|
||||
@Column({ length: 32 })
|
||||
@Index({ unique: true })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 30 })
|
||||
@@ -33,15 +35,33 @@ export class User {
|
||||
@Column({ nullable: true, type: 'char', length: 64 })
|
||||
password_hash: string;
|
||||
|
||||
@Column({ nullable: true, length: 254 })// RFC 5321
|
||||
@Index({ unique: true })
|
||||
email: string;
|
||||
@Column({
|
||||
nullable: true,
|
||||
length: 254,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
}
|
||||
})// RFC 5321
|
||||
email: string | null;
|
||||
|
||||
@Column({ nullable: true, length: 20 })// China Mainland
|
||||
@Index({ unique: true })
|
||||
phone: string;
|
||||
@Column({
|
||||
nullable: true,
|
||||
length: 20,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
}
|
||||
})// China Mainland
|
||||
phone: string | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({
|
||||
nullable: true,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
}
|
||||
})
|
||||
avatar: string;
|
||||
|
||||
@CreateDateColumn({ precision: 3 })
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { createHash } from 'crypto';
|
||||
import { QueryFailedError, Repository } from 'typeorm';
|
||||
import { createHash, ECDH } from 'crypto';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>;
|
||||
@@ -31,8 +31,14 @@ export class UserService {
|
||||
if (!existingUser) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
try {
|
||||
Object.assign(existingUser, user);
|
||||
return this.userRepository.save(existingUser);
|
||||
return await this.userRepository.save(existingUser);
|
||||
} catch (error) {
|
||||
if (error instanceof QueryFailedError) {
|
||||
throw new ConflictException(this.getDuplicateErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(userId: string): Promise<void> {
|
||||
@@ -61,4 +67,18 @@ export class UserService {
|
||||
user.salt = salt;
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
private getDuplicateErrorMessage(error: QueryFailedError): string {
|
||||
// 根据具体的错误信息返回友好的提示
|
||||
if (error.message.includes('IDX_user_username')) {
|
||||
return '用户名已被使用';
|
||||
}
|
||||
if (error.message.includes('IDX_user_email')) {
|
||||
return '邮箱已被使用';
|
||||
}
|
||||
if (error.message.includes('IDX_user_phone')) {
|
||||
return '手机号已被使用';
|
||||
}
|
||||
return '数据已存在,请检查输入';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,34 @@ import { Label } from "@/components/ui/label"
|
||||
import { useUser } from "@/hooks/admin/user/use-user";
|
||||
import { User } from "@/lib/types/user";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { updateUser } from "@/lib/api/admin/user";
|
||||
import { AdminApi } from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function UserInfoEditor({
|
||||
onClose,
|
||||
onUserUpdate,
|
||||
userId,
|
||||
}: {
|
||||
onClose: () => void,
|
||||
onUserUpdate: (user: User) => void,
|
||||
userId: string
|
||||
}) {
|
||||
const { user, isLoading, error } = userId ? useUser(userId) : {};
|
||||
const handleSave = async (user: updateUser) => {
|
||||
try {
|
||||
const res = await AdminApi.user.update(userId, user);
|
||||
if (res) {
|
||||
toast.success("保存成功");
|
||||
onUserUpdate(res);
|
||||
onClose();
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message || "保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer open={!!userId} onClose={onClose} >
|
||||
@@ -36,7 +55,14 @@ export function UserInfoEditor({
|
||||
</DrawerHeader>
|
||||
|
||||
{user && <ProfileForm className="px-4" user={user} onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget);
|
||||
handleSave({
|
||||
username: formData.get("username")?.toString()!,
|
||||
nickname: formData.get("nickname")?.toString()!,
|
||||
email: formData.get("email")?.toString() || null,
|
||||
phone: formData.get("phone")?.toString() || null,
|
||||
})
|
||||
}} />}
|
||||
|
||||
{isLoading &&
|
||||
@@ -59,24 +85,24 @@ function ProfileForm({ className, user, ...props }: React.ComponentProps<"form">
|
||||
return (
|
||||
<form className={cn("grid items-start gap-4", className)} {...props}>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">UserId</Label>
|
||||
<Input id="email" defaultValue={user.userId} disabled />
|
||||
<Label htmlFor="userId">UserId</Label>
|
||||
<Input id="userId" name="userId" defaultValue={user.userId} disabled />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">账户</Label>
|
||||
<Input id="username" defaultValue={user.username} />
|
||||
<Input id="username" name="username" defaultValue={user.username} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="nickname">昵称</Label>
|
||||
<Input id="nickname" defaultValue={user.nickname} />
|
||||
<Input id="nickname" name="nickname" defaultValue={user.nickname} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">电子邮箱</Label>
|
||||
<Input id="email" defaultValue={user.email} />
|
||||
<Input id="email" name="email" defaultValue={user.email} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">手机号</Label>
|
||||
<Input id="phone" defaultValue={user.phone} />
|
||||
<Input id="phone" name="phone" defaultValue={user.phone} />
|
||||
</div>
|
||||
<Button type="submit">保存</Button>
|
||||
</form>
|
||||
|
||||
@@ -67,7 +67,12 @@ export default function Page() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<UserInfoEditor onClose={() => setEditorUserId('')} userId={editorUserId} />
|
||||
<UserInfoEditor onClose={() => setEditorUserId('')} userId={editorUserId} onUserUpdate={(user) => {
|
||||
const index = users.findIndex((u) => u.userId === user.userId);
|
||||
if (index !== -1) {
|
||||
users[index] = user;
|
||||
}
|
||||
}} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
3
tone-page-web/lib/api/admin/user/create.ts
Normal file
3
tone-page-web/lib/api/admin/user/create.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function create() {
|
||||
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './list';
|
||||
export * from './get';
|
||||
export * from './create';
|
||||
export * from './update';
|
||||
16
tone-page-web/lib/api/admin/user/update.ts
Normal file
16
tone-page-web/lib/api/admin/user/update.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { User } from "@/lib/types/user";
|
||||
import fetcher from "../../fetcher";
|
||||
|
||||
export type updateUser = {
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
}
|
||||
|
||||
export async function update(userId: string, user: updateUser) {
|
||||
return fetcher<User>(`/api/admin/user/${userId}`, {
|
||||
body: JSON.stringify(user),
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user