feat: 更新用户显示名称格式化逻辑,优化头像获取方式,增强用户界面

This commit is contained in:
2025-09-22 21:07:48 +08:00
parent 42496cfe5a
commit aeff954481
5 changed files with 76 additions and 15 deletions

View File

@ -30,7 +30,7 @@ import {
} from "@/components/ui/sidebar"
import { getGravatarFromUser } from "@/utils/common/gravatar"
import { getFallbackAvatarFromUsername } from "@/utils/common/username"
import { formatDisplayName, getFallbackAvatarFromUsername } from "@/utils/common/username"
import { useAuth } from "@/contexts/auth-context"
import { userLogout } from "@/api/user"
import { toast } from "sonner"
@ -61,7 +61,7 @@ export function NavUser() {
<AvatarFallback className="rounded-full">{getFallbackAvatarFromUsername(user.nickname || user.username)}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}({user.username})</span>
<span className="truncate font-medium">{formatDisplayName(user)}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
@ -82,7 +82,7 @@ export function NavUser() {
<AvatarFallback className="rounded-full">{getFallbackAvatarFromUsername(user.nickname || user.username)}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}({user.username})</span>
<span className="truncate font-medium">{formatDisplayName(user)}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>

View File

@ -1,6 +1,7 @@
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { SidebarTrigger } from "@/components/ui/sidebar"
import Link from "next/link"
export function SiteHeader({ title = "Title" }: { title?: string }) {
return (
@ -14,14 +15,14 @@ export function SiteHeader({ title = "Title" }: { title?: string }) {
<h1 className="text-base font-medium">{title}</h1>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
<Link
href="https://github.com/snowykami/neo-blog"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
GitHub
</a>
</Link>
</Button>
</div>
</div>

View File

@ -15,7 +15,7 @@ import { useToLogin } from "@/hooks/use-route";
import { CircleUser } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { getGravatarFromUser } from "@/utils/common/gravatar";
import { getFallbackAvatarFromUsername } from "@/utils/common/username";
import { formatDisplayName, getFallbackAvatarFromUsername } from "@/utils/common/username";
import { useAuth } from "@/contexts/auth-context";
export function AvatarWithDropdownMenu() {
@ -39,8 +39,17 @@ export function AvatarWithDropdownMenu() {
</Avatar> : <CircleUser className="h-8 w-8" />}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuContent className="w-auto" align="start">
<DropdownMenuLabel>
{user && <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{formatDisplayName(user)}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
</div>}
</DropdownMenuLabel>
<DropdownMenuGroup>
{user && <DropdownMenuItem asChild>
<Link href={`/u/${user?.username}`}>Profile</Link>
@ -51,7 +60,7 @@ export function AvatarWithDropdownMenu() {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={user ? handleLogout : toLogin}>
{user ? `Logout (${user.username})` : "Login"}
{user ? "Logout" : "Login"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,12 +1,12 @@
export interface User {
id: number;
username: string;
nickname: string;
avatarUrl: string;
nickname?: string;
avatarUrl?: string;
email: string;
gender: string;
gender?: string;
role: string;
language: string;
language?: string;
}
export enum Role {

View File

@ -1,5 +1,21 @@
import { User } from "@/models/user";
/**
* Returns a single-character fallback avatar derived from a username.
*
* Rules:
* - If `username` is falsy (empty string, null, or undefined), returns `"N"`.
* - If the first character is an ASCII letter, returns the uppercase form of that character.
* - Otherwise returns the first character as-is.
*
* @param username - The username to derive the avatar character from.
* @returns A single character to use as a fallback avatar.
*
* @example
* getFallbackAvatarFromUsername("alice"); // "A"
* getFallbackAvatarFromUsername("1user"); // "1"
* getFallbackAvatarFromUsername(""); // "N"
*/
export function getFallbackAvatarFromUsername(username: string): string {
if (!username) {
return "N";
@ -11,6 +27,24 @@ export function getFallbackAvatarFromUsername(username: string): string {
return firstChar;
}
/**
* Returns the first-character avatar for a User, preferring nickname over username.
*
* Behavior:
* - If `user.nickname` is present, derives the character from the nickname.
* - Else if `user.username` is present, derives the character from the username.
* - If neither is present, returns `"N"`.
*
* This function delegates the actual character selection logic to `getFallbackAvatarFromUsername`.
*
* @param user - The user object to extract the character from.
* @returns A single character derived from the user's nickname or username, or `"N"` if neither is present.
*
* @example
* getFirstCharFromUser({ nickname: "Bob", username: "bob123" }); // "B"
* getFirstCharFromUser({ username: "1user" }); // "1"
* getFirstCharFromUser({}); // "N"
*/
export function getFirstCharFromUser(user: User): string {
if (user.nickname) {
return getFallbackAvatarFromUsername(user.nickname);
@ -20,3 +54,20 @@ export function getFirstCharFromUser(user: User): string {
}
return "N";
}
/**
* Formats a user's display name.
*
* - If the user has a `nickname`, returns `${nickname}(${username})`.
* - Otherwise returns the `username`.
*
* @param user - The user whose display name is to be formatted.
* @returns The formatted display name. If `username` is missing, the returned value may be falsy.
*
* @example
* formatDisplayName({ nickname: "Sam", username: "sam42" }); // "Sam(sam42)"
* formatDisplayName({ username: "sam42" }); // "sam42"
*/
export function formatDisplayName(user: User) :string {
return user?.nickname ? `${user.nickname}(${user.username})` : user.username;
}