Skip to content

Commit ad9bcb1

Browse files
committed
✨ feat: 添加个人页
1 parent c2ef0bf commit ad9bcb1

File tree

9 files changed

+165
-16
lines changed

9 files changed

+165
-16
lines changed

game/backend/src/routes/api.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Router, Request, Response } from "express";
22
import { Controller } from "../controller";
33
import { login as fishpiLogin, register as fishpiRegister, updateUserInfo } from "../login/fishpi";
4-
import { User } from "@/entities";
4+
import { User, UserRepo } from "@/entities";
55

66
export interface GameContext {
77
controller?: Controller;
@@ -41,6 +41,15 @@ const createRoutes = (game: GameContext, gameName: string) => {
4141
});
4242
});
4343

44+
router.get("/user/:username", async (req: Request, res: Response) => {
45+
const user = await UserRepo.findOneBy({ username: req.params.username });
46+
if (user) {
47+
res.json({ code: 0, data: user });
48+
} else {
49+
res.json({ code: 1, message: "用户不存在" });
50+
}
51+
});
52+
4453
router.get("/message", (req: Request, res: Response) => {
4554
res.json({
4655
code: 0,

game/frontend/src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export const api = {
6767
getUserInfo(): Promise<User> {
6868
return instance.get('/info').then((data: any) => new User(data.player))
6969
},
70+
getUser(username: string): Promise<User> {
71+
return instance.get(`/user/${username}`).then((data: any) => new User(data))
72+
},
7073
getMessages(): Promise<{ messages: { data: string, sender: Player, createdAt: number }[] }> {
7174
return instance.get('/message')
7275
},

game/frontend/src/components/common/PlayerList.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
<li
44
v-for="p in players.filter(p => p.role === 'player')"
55
:key="p.id"
6-
class="flex items-center gap-2 text-sm p-1 rounded hover:bg-base-200/50"
6+
class="flex items-center gap-2 text-sm p-1 rounded hover:bg-base-200/50 cursor-pointer"
77
:class="{ 'opacity-50': p.status == PlayerStatus.offline }"
8+
@click="$router.push(`/u/${p.attributes.username}`)"
89
>
910
<slot :player="p">
1011
<span>[{{ p.isReady ? '已准备' : '未准备' }}]</span>

game/frontend/src/components/common/RoomControls.vue

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
11
<template>
2-
<div class="flex flex-row items-center gap-2 py-2">
2+
<div class="md:flex flex-row items-center gap-2 pl-2 md:text-[20px] text-[10px] grid grid-cols-2 md:w-auto w-[7em]">
33
<!-- Waiting: Player Actions -->
4-
<div v-if="!isPlaying && roomPlayer.role === PlayerRole.player" class="group flex gap-2">
5-
<button class="btn btn-circle btn-soft tooltip tooltip-left"
4+
<template v-if="!isPlaying && roomPlayer.role === PlayerRole.player">
5+
<button class="btn btn-circle md:btn-lg btn-soft tooltip tooltip-left"
66
@click="game?.leaveRoom(roomPlayer.room.id)"
77
:disabled="roomPlayer.isReady"
88
data-tip="离开房间"
99
>
1010
<Icon icon="mdi:logout" />
1111
</button>
12-
<button class="btn btn-circle btn-soft tooltip tooltip-left"
12+
<button class="btn btn-circle md:btn-lg btn-soft tooltip tooltip-left"
1313
@click="game?.leaveSeat(roomPlayer.room.id)"
1414
:disabled="roomPlayer.isReady"
1515
data-tip="离开座位"
1616
>
1717
<Icon icon="mdi:gamepad-off" />
1818
</button>
19-
<button class="btn btn-accent btn-circle tooltip tooltip-left"
19+
<button class="btn btn-accent md:btn-lg btn-circle tooltip tooltip-left"
2020
@click="game?.ready(roomPlayer.room.id, !roomPlayer.isReady)"
2121
:data-tip="roomPlayer.isReady ? '取消' : '准备'"
2222
>
2323
<Icon :icon="roomPlayer.isReady ? 'mdi:close' : 'mdi:check'" />
2424
</button>
25-
<button class="btn btn-primary btn-circle tooltip tooltip-left"
25+
<button class="btn btn-primary md:btn-lg btn-circle tooltip tooltip-left"
2626
@click="game?.startGame(roomPlayer.room.id)"
2727
:disabled="!isAllReady"
2828
data-tip="开始游戏"
2929
>
3030
<Icon icon="mdi:play" />
3131
</button>
32-
</div>
32+
</template>
3333

3434
<!-- Watcher Actions -->
35-
<div v-if="roomPlayer.role === PlayerRole.watcher" class="group flex gap-2">
36-
<button class="btn btn-circle btn-soft tooltip tooltip-left"
35+
<template v-if="roomPlayer.role === PlayerRole.watcher">
36+
<button class="btn btn-circle md:btn-lg btn-soft tooltip tooltip-left"
3737
@click="game?.leaveRoom(roomPlayer.room.id)"
3838
:disabled="roomPlayer.isReady"
3939
data-tip="离开房间"
4040
>
4141
<Icon icon="mdi:logout" />
4242
</button>
43-
<button class="btn btn-circle btn-soft tooltip tooltip-left"
43+
<button class="btn btn-circle md:btn-lg btn-soft tooltip tooltip-left"
4444
v-if="!isRoomFull && !isPlaying"
4545
@click="game?.joinRoom(roomPlayer.room.id)"
4646
data-tip="加入游戏"
4747
>
4848
<Icon icon="mdi:google-gamepad" />
4949
</button>
50-
</div>
50+
</template>
5151
<!-- Extra Slot -->
5252
<slot />
5353
</div>
@@ -74,3 +74,11 @@ const isRoomFull = computed(() => {
7474
return props.roomPlayer.room.players.filter((p: any) => p.role === 'player').length >= props.roomPlayer.room.size
7575
})
7676
</script>
77+
<style lang="less" scoped>
78+
.btn {
79+
--size: 2em;
80+
}
81+
:deep(.btn) {
82+
--size: 2em;
83+
}
84+
</style>

game/frontend/src/layout/Default.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@
154154
<li
155155
v-for="p in gameStore.players"
156156
:key="p.id"
157-
class="text-sm text-base-content/80 flex items-center gap-2 p-2 hover:bg-base-300 rounded transition-colors"
157+
class="text-sm text-base-content/80 flex items-center gap-2 p-2 hover:bg-base-300 rounded transition-colors cursor-pointer"
158+
@click="$router.push(`/u/${p.attributes.username}`)"
158159
>
159160
<div
160161
class="w-6 h-6 rounded-full bg-base-300 flex items-center justify-center text-xs overflow-hidden border border-base-content/10"

game/frontend/src/router/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ const router = createRouter({
4545
component: () => import('@/views/Room.vue'),
4646
meta: { requiresAuth: true }
4747
},
48+
{
49+
path: 'u/:username',
50+
name: 'user',
51+
component: () => import('@/views/Profile.vue'),
52+
meta: { requiresAuth: true }
53+
},
4854
]
4955
}
5056
]

game/frontend/src/views/Lite.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<!-- 创建房间 -->
66
<CreateRoom v-if="!gameStore.roomPlayer" />
77
<RoomControlsLite
8-
v-if="gameStore.game"
8+
v-if="gameStore.game && gameStore.roomPlayer"
99
:game="gameStore.game"
1010
:room-player="gameStore.roomPlayer"
1111
/>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<script lang="ts" setup>
2+
import { api, User } from '@/api';
3+
import { ref, watch } from 'vue';
4+
import { useRoute, useRouter } from 'vue-router';
5+
import Icon from '@/components/common/Icon.vue';
6+
7+
const user = ref<User>();
8+
const route = useRoute();
9+
const router = useRouter();
10+
const error = ref<string>('正在加载中...');
11+
12+
watch(
13+
() => route.params.username,
14+
() => {
15+
load();
16+
},
17+
{ immediate: true }
18+
);
19+
20+
function load() {
21+
user.value = undefined;
22+
error.value = '正在加载中...';
23+
api.getUser(route.params.username as string).then(u => {
24+
user.value = u;
25+
}).catch(() => {
26+
error.value = '用户不存在';
27+
});
28+
}
29+
30+
load();
31+
</script>
32+
33+
<template>
34+
<main
35+
v-if="!user"
36+
class="flex-1 overflow-auto bg-base-100 w-full flex items-center justify-center"
37+
>
38+
<span class="text-lg text-base-content/70">{{ error }}</span>
39+
</main>
40+
<section v-else class="h-full flex flex-col w-full bg-base-200">
41+
<header
42+
class="flex justify-between items-center px-4 py-2 bg-base-100 shadow-sm z-10"
43+
>
44+
<button class="btn btn-ghost btn-sm gap-2" @click="router.push('/')">
45+
<Icon icon="mdi:arrow-left" />
46+
返回
47+
</button>
48+
<h1 class="text-lg font-bold">用户资料</h1>
49+
<div class="w-16"></div> <!-- Spacer for centering -->
50+
</header>
51+
52+
<main class="flex-1 overflow-auto p-4 space-y-6 max-w-4xl mx-auto w-full">
53+
<!-- User Info Card -->
54+
<div class="card bg-base-100 shadow-xl">
55+
<div class="card-body flex-row items-center gap-6">
56+
<div class="avatar placeholder">
57+
<div class="bg-neutral text-neutral-content rounded-full w-24">
58+
<img v-if="user.avatar" :src="user.avatar" :alt="user.nickname" />
59+
<span v-else class="text-3xl">{{ user.nickname.charAt(0).toUpperCase() }}</span>
60+
</div>
61+
</div>
62+
<div class="flex flex-col gap-1">
63+
<h2 class="card-title text-2xl flex items-center gap-2">
64+
{{ user.nickname }}
65+
<div v-if="user.isAdmin" class="badge badge-primary badge-sm">管理员</div>
66+
</h2>
67+
<p class="text-base-content/70">@{{ user.username }}</p>
68+
<div class="flex items-center gap-2 text-sm text-base-content/60 mt-1">
69+
<img src="/fishpi.svg" class="w-[1.5em] group-hover:scale-110 transition-transform" />
70+
<a :href="`https://fishpi.cn/member/${user.username}`" target="_blank" rel="noopener noreferrer">
71+
访问摸鱼派主页
72+
</a>
73+
</div>
74+
</div>
75+
</div>
76+
</div>
77+
78+
<!-- Stats Section (Placeholder) -->
79+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
80+
<div class="card bg-base-100 shadow-xl">
81+
<div class="card-body">
82+
<h3 class="card-title text-lg mb-2">
83+
<Icon icon="mdi:chart-box" class="text-primary" />
84+
胜负统计
85+
</h3>
86+
<div class="flex items-center justify-center h-32 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
87+
<span class="text-base-content/50">暂无数据 (开发中)</span>
88+
</div>
89+
</div>
90+
</div>
91+
92+
<div class="card bg-base-100 shadow-xl">
93+
<div class="card-body">
94+
<h3 class="card-title text-lg mb-2">
95+
<Icon icon="mdi:trophy" class="text-warning" />
96+
成就
97+
</h3>
98+
<div class="flex items-center justify-center h-32 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
99+
<span class="text-base-content/50">暂无数据 (开发中)</span>
100+
</div>
101+
</div>
102+
</div>
103+
</div>
104+
105+
<!-- Game History Section (Placeholder) -->
106+
<div class="card bg-base-100 shadow-xl">
107+
<div class="card-body">
108+
<h3 class="card-title text-lg mb-4">
109+
<Icon icon="mdi:history" class="text-secondary" />
110+
游玩记录
111+
</h3>
112+
113+
<div class="flex flex-col items-center justify-center py-12 bg-base-200 rounded-lg border-2 border-dashed border-base-300">
114+
<Icon icon="mdi:gamepad-variant-outline" class="text-4xl text-base-content/30 mb-2" />
115+
<span class="text-base-content/50">暂无游玩记录</span>
116+
</div>
117+
</div>
118+
</div>
119+
</main>
120+
</section>
121+
</template>

game/frontend/src/views/Room.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ const hasLiteComponent = computed(() => {
133133
>
134134
<button
135135
v-if="hasLiteComponent"
136-
class="btn btn-circle btn-soft hidden md:flex tooltip tooltip-left"
136+
class="btn btn-circle md:btn-lg btn-soft hidden md:flex tooltip tooltip-left"
137137
data-tip="弹出"
138138
@click="openSmallWindow('/#/lite')"
139139
>

0 commit comments

Comments
 (0)