diff --git a/packages/app/src/components/UserCard.vue b/packages/app/src/components/UserCard.vue
index 7e13156..ba64ad4 100644
--- a/packages/app/src/components/UserCard.vue
+++ b/packages/app/src/components/UserCard.vue
@@ -16,8 +16,8 @@
-
-
+
+
{{ maskedPhone }}
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
{{ stats?.totalBookings ?? 0 }}
总训练(次)
@@ -65,6 +78,7 @@ import { MembershipStatus } from '@mp-pilates/shared'
const props = defineProps<{
loggedIn: boolean
+ hasProfile: boolean
user: UserProfileResponse | null
stats: UserStatsResponse | null
memberships?: readonly MembershipWithCardType[]
@@ -235,6 +249,35 @@ function handleLogin() {
color: rgba(255, 255, 255, 0.75);
}
+ // ── Loading state ──
+ &__loading {
+ display: flex;
+ align-items: center;
+ gap: $spacing-md;
+ }
+
+ &__avatar-skeleton {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.3);
+ }
+
+ &__nickname-skeleton {
+ width: 160rpx;
+ height: 36rpx;
+ border-radius: $radius-sm;
+ background: rgba(255, 255, 255, 0.3);
+ }
+
+ &__phone-skeleton {
+ width: 120rpx;
+ height: 26rpx;
+ border-radius: $radius-sm;
+ background: rgba(255, 255, 255, 0.2);
+ margin-top: 8rpx;
+ }
+
// ── Stats row ──
&__stats {
display: flex;
diff --git a/packages/app/src/pages/profile/index.vue b/packages/app/src/pages/profile/index.vue
index 65eb085..d24d16d 100644
--- a/packages/app/src/pages/profile/index.vue
+++ b/packages/app/src/pages/profile/index.vue
@@ -3,6 +3,7 @@
{
const token = ref(uni.getStorageSync('token') as string || '')
// Getters
- const loggedIn = computed(() => !!token.value && !!user.value)
+ // loggedIn: 是否已认证(token 存在),与 user 数据是否已加载无关
+ const loggedIn = computed(() => !!token.value)
+ // hasProfile: user 数据是否已加载(用于 UI 展示判断)
+ const hasProfile = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === UserRole.ADMIN)
const activeMemberships = computed(() =>
memberships.value.filter((m) => m.status === MembershipStatus.ACTIVE),
@@ -91,6 +94,7 @@ export const useUserStore = defineStore('user', () => {
memberships,
token,
loggedIn,
+ hasProfile,
isAdmin,
activeMemberships,
hasValidMembership,
diff --git a/packages/server/src/auth/__tests__/auth.service.spec.ts b/packages/server/src/auth/__tests__/auth.service.spec.ts
index 1e7f06c..d06bed0 100644
--- a/packages/server/src/auth/__tests__/auth.service.spec.ts
+++ b/packages/server/src/auth/__tests__/auth.service.spec.ts
@@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'
import { JwtService } from '@nestjs/jwt'
import { UnauthorizedException } from '@nestjs/common'
import { UserRole } from '@mp-pilates/shared'
-import { AuthService } from '../auth.service'
+import { AuthService, RANDOM_FN_TOKEN } from '../auth.service'
import { WechatService } from '../wechat.service'
import { PrismaService } from '../../prisma/prisma.service'
@@ -12,13 +12,14 @@ const OPENID = 'test_openid_123'
const SESSION_KEY = 'test_session_key'
const USER_ID = 'user-uuid-001'
const JWT_TOKEN = 'signed.jwt.token'
+const TEST_NICKNAME = '优雅普拉提'
const mockUser = {
id: USER_ID,
openid: OPENID,
unionid: null,
phone: null,
- nickname: '',
+ nickname: TEST_NICKNAME,
avatarUrl: null,
role: UserRole.MEMBER,
createdAt: new Date(),
@@ -57,6 +58,7 @@ describe('AuthService', () => {
{ provide: PrismaService, useValue: mockPrismaService },
{ provide: WechatService, useValue: mockWechatService },
{ provide: JwtService, useValue: mockJwtService },
+ { provide: RANDOM_FN_TOKEN, useValue: () => 0 }, // deterministic nickname
],
}).compile()
@@ -89,7 +91,7 @@ describe('AuthService', () => {
where: { openid: OPENID },
})
expect(mockPrismaService.user.create).toHaveBeenCalledWith({
- data: { openid: OPENID },
+ data: { openid: OPENID, nickname: TEST_NICKNAME },
})
expect(result.user).toEqual(mockUser)
})
@@ -107,7 +109,7 @@ describe('AuthService', () => {
await authService.login(loginCode)
expect(mockPrismaService.user.create).toHaveBeenCalledWith({
- data: { openid: OPENID, unionid },
+ data: { openid: OPENID, unionid, nickname: TEST_NICKNAME },
})
})
diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts
index a04463b..8416dd5 100644
--- a/packages/server/src/auth/auth.service.ts
+++ b/packages/server/src/auth/auth.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, UnauthorizedException } from '@nestjs/common'
+import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { User } from '@prisma/client'
import { UserRole } from '@mp-pilates/shared'
@@ -22,12 +22,39 @@ export interface JwtPayload {
*/
const sessionKeyStore = new Map()
+export const RANDOM_FN_TOKEN = 'RANDOM_FN_TOKEN'
+
+const DEFAULT_NICKNAMES = [
+ '优雅普拉提',
+ '柔韧时光',
+ '轻盈姿态',
+ '身心合一',
+ '舒展生活',
+ '静享流动',
+ '普拉提修行者',
+ '姿态雕塑师',
+ '呼吸艺术家',
+ '柔美力量',
+ '线条雕刻师',
+ '优雅行者',
+ '轻盈韵律',
+ '内在平和',
+ '舒展之美',
+]
+
+function generateDefaultNickname(
+ randomFn: () => number = Math.random,
+): string {
+ return DEFAULT_NICKNAMES[Math.floor(randomFn() * DEFAULT_NICKNAMES.length)]
+}
+
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly wechatService: WechatService,
+ @Inject(RANDOM_FN_TOKEN) private readonly randomFn: () => number = Math.random,
) {}
async login(code: string): Promise {
@@ -44,6 +71,7 @@ export class AuthService {
data: {
openid,
...(unionid !== undefined && { unionid }),
+ nickname: generateDefaultNickname(this.randomFn),
},
}))