完成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 {
|
export class CreateDto {
|
||||||
@IsOptional()
|
@ValidateIf(o => o.username !== null)
|
||||||
@IsString()
|
@IsString({ message: '用户名不得为空' })
|
||||||
username?: string;
|
@Length(6, 32, { message: '用户名长度只能为6~32' })
|
||||||
|
username: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf(o => o.username !== null)
|
||||||
@IsString()
|
@IsString({ message: '昵称不得为空' })
|
||||||
nickname?: string;
|
@Length(6, 30, { message: '昵称长度只能为6~30' })
|
||||||
|
nickname: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf(o => o.username !== null)
|
||||||
@IsString()
|
@IsString({ message: '邮箱不得为空' })
|
||||||
email?: string;
|
@Length(6, 254, { message: '邮箱长度只能为6~254' })
|
||||||
|
email: string | null;
|
||||||
|
|
||||||
@IsOptional()
|
@ValidateIf(o => o.username !== null)
|
||||||
@IsString()
|
@IsString({ message: '手机号不得为空' })
|
||||||
phone?: string;
|
@Length(11, 11, { message: '手机号长度只能为11' })
|
||||||
|
phone: string | null;
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
avatar?: string;
|
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
import { IsOptional, IsString } from "class-validator";
|
import { IsEmail, IsOptional, IsString, Length, Matches, ValidateIf } from "class-validator";
|
||||||
|
|
||||||
export class UpdateDto {
|
export class UpdateDto {
|
||||||
@IsOptional()
|
@IsString({ message: '用户名不得为空' })
|
||||||
@IsString()
|
@Length(6, 32, { message: '用户名长度只能为6~32' })
|
||||||
username?: string;
|
username: string;
|
||||||
|
|
||||||
|
@IsString({ message: '昵称不得为空' })
|
||||||
|
@Length(6, 30, { message: '昵称长度只能为6~30' })
|
||||||
|
nickname: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsEmail({}, { message: '请输入有效的邮箱地址', always: false })
|
||||||
nickname?: string;
|
@Length(6, 254, {
|
||||||
|
message: '邮箱长度只能为6~254',
|
||||||
@IsOptional()
|
// 仅在值不为 null 或 undefined 时验证
|
||||||
@IsString()
|
always: false
|
||||||
|
})
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional() // 标记字段为可选
|
||||||
@IsString()
|
@IsString({ message: '手机号不得为空', always: false })
|
||||||
|
@Matches(/^1[3456789]\d{9}$/, {
|
||||||
|
message: '请输入有效的手机号码',
|
||||||
|
// 仅在值不为 null 或 undefined 时验证
|
||||||
|
always: false
|
||||||
|
})
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
avatar?: string;
|
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,18 @@ import { BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@Entity()
|
@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 {
|
export class User {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column('uuid', { unique: true, default: () => 'gen_random_uuid()' })
|
@Column('uuid', { default: () => 'gen_random_uuid()' })
|
||||||
@Index({ unique: true })
|
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
@Column({ length: 32 })
|
@Column({ length: 32 })
|
||||||
@Index({ unique: true })
|
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@Column({ length: 30 })
|
@Column({ length: 30 })
|
||||||
@@ -33,15 +35,33 @@ export class User {
|
|||||||
@Column({ nullable: true, type: 'char', length: 64 })
|
@Column({ nullable: true, type: 'char', length: 64 })
|
||||||
password_hash: string;
|
password_hash: string;
|
||||||
|
|
||||||
@Column({ nullable: true, length: 254 })// RFC 5321
|
@Column({
|
||||||
@Index({ unique: true })
|
nullable: true,
|
||||||
email: string;
|
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
|
@Column({
|
||||||
@Index({ unique: true })
|
nullable: true,
|
||||||
phone: string;
|
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;
|
avatar: string;
|
||||||
|
|
||||||
@CreateDateColumn({ precision: 3 })
|
@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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { QueryFailedError, Repository } from 'typeorm';
|
||||||
import { createHash } from 'crypto';
|
import { createHash, ECDH } from 'crypto';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>;
|
type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>;
|
||||||
@@ -31,8 +31,14 @@ export class UserService {
|
|||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new BadRequestException('User not found');
|
throw new BadRequestException('User not found');
|
||||||
}
|
}
|
||||||
Object.assign(existingUser, user);
|
try {
|
||||||
return this.userRepository.save(existingUser);
|
Object.assign(existingUser, user);
|
||||||
|
return await this.userRepository.save(existingUser);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof QueryFailedError) {
|
||||||
|
throw new ConflictException(this.getDuplicateErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(userId: string): Promise<void> {
|
async delete(userId: string): Promise<void> {
|
||||||
@@ -61,4 +67,18 @@ export class UserService {
|
|||||||
user.salt = salt;
|
user.salt = salt;
|
||||||
return this.userRepository.save(user);
|
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 { useUser } from "@/hooks/admin/user/use-user";
|
||||||
import { User } from "@/lib/types/user";
|
import { User } from "@/lib/types/user";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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({
|
export function UserInfoEditor({
|
||||||
onClose,
|
onClose,
|
||||||
|
onUserUpdate,
|
||||||
userId,
|
userId,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
|
onUserUpdate: (user: User) => void,
|
||||||
userId: string
|
userId: string
|
||||||
}) {
|
}) {
|
||||||
const { user, isLoading, error } = userId ? useUser(userId) : {};
|
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 (
|
return (
|
||||||
<Drawer open={!!userId} onClose={onClose} >
|
<Drawer open={!!userId} onClose={onClose} >
|
||||||
@@ -36,7 +55,14 @@ export function UserInfoEditor({
|
|||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
{user && <ProfileForm className="px-4" user={user} onSubmit={(e) => {
|
{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 &&
|
{isLoading &&
|
||||||
@@ -59,24 +85,24 @@ function ProfileForm({ className, user, ...props }: React.ComponentProps<"form">
|
|||||||
return (
|
return (
|
||||||
<form className={cn("grid items-start gap-4", className)} {...props}>
|
<form className={cn("grid items-start gap-4", className)} {...props}>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="email">UserId</Label>
|
<Label htmlFor="userId">UserId</Label>
|
||||||
<Input id="email" defaultValue={user.userId} disabled />
|
<Input id="userId" name="userId" defaultValue={user.userId} disabled />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="username">账户</Label>
|
<Label htmlFor="username">账户</Label>
|
||||||
<Input id="username" defaultValue={user.username} />
|
<Input id="username" name="username" defaultValue={user.username} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="nickname">昵称</Label>
|
<Label htmlFor="nickname">昵称</Label>
|
||||||
<Input id="nickname" defaultValue={user.nickname} />
|
<Input id="nickname" name="nickname" defaultValue={user.nickname} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="email">电子邮箱</Label>
|
<Label htmlFor="email">电子邮箱</Label>
|
||||||
<Input id="email" defaultValue={user.email} />
|
<Input id="email" name="email" defaultValue={user.email} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="phone">手机号</Label>
|
<Label htmlFor="phone">手机号</Label>
|
||||||
<Input id="phone" defaultValue={user.phone} />
|
<Input id="phone" name="phone" defaultValue={user.phone} />
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit">保存</Button>
|
<Button type="submit">保存</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -67,7 +67,12 @@ export default function Page() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</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 './list';
|
||||||
export * from './get';
|
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