Skip to content

Commit de7ec5b

Browse files
authored
Merge pull request #6 from COSCUP/feature/my-profile
feat: add my profile
2 parents a2e6ce3 + 13e3a5b commit de7ec5b

File tree

11 files changed

+307
-15
lines changed

11 files changed

+307
-15
lines changed

public/assets/小啄_01.png

9.41 KB
Loading

public/assets/小啄_02.png

13.5 KB
Loading

src/App.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ const goToGameScene = () => {
2828

2929
<div class="bottom-bar">
3030
<button class="button button-sponsor" @click="goToSponsorList">
31-
<Icon icon="mdi:hand-heart-outline" class="icon" />
32-
<span>贊助商列表</span>
31+
<Icon icon="tabler:building-store" class="icon" />
32+
<span>攤位列表</span>
3333
</button>
3434

3535
<button class="button button-game" v-if="route.name !== 'game'" @click="goToGameScene">

src/components/MyProfile.vue

Lines changed: 250 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,259 @@
11
<script setup lang="ts">
2+
import { ref, onMounted, onUnmounted, computed } from 'vue'
3+
import { Icon } from '@iconify/vue'
4+
import { achievements } from '../data/AchievementsData';
5+
6+
const player = ref({
7+
avatar: '/assets/小啄_02.png',
8+
nickname: '鱈魚',
9+
title: '新手小啄',
10+
points: 0,
11+
achievements: achievements,
12+
});
13+
14+
const availableTitles = computed(() => {
15+
const titles = [{ id: 0, label: '新手小啄', icon: '' }];
16+
player.value.achievements.forEach(achievement => {
17+
if (achievement.unlocked) {
18+
titles.push({
19+
id: achievement.id,
20+
label: achievement.label,
21+
icon: achievement.icon
22+
});
23+
}
24+
});
25+
return titles;
26+
});
27+
28+
// This is for test
29+
const handleKeydown = (event: KeyboardEvent) => {
30+
const key = parseInt(event.key);
31+
if (key >= 1 && key <= player.value.achievements.length) {
32+
const medalToUnlock = player.value.achievements.find(m => m.id === key);
33+
if (medalToUnlock && !medalToUnlock.unlocked) {
34+
medalToUnlock.unlocked = true;
35+
}
36+
}
37+
};
38+
39+
onMounted(() => {
40+
window.addEventListener('keydown', handleKeydown);
41+
});
42+
43+
onUnmounted(() => {
44+
window.removeEventListener('keydown', handleKeydown);
45+
});
246
</script>
347

448
<template>
5-
<main id="myProfile">
6-
<h2>我的資料</h2>
7-
</main>
49+
<div id="myProfile">
50+
<div class="avatar-section">
51+
<div class="avatar-container">
52+
<img :src="player.avatar" alt="Player Avatar" class="avatar-image">
53+
</div>
54+
</div>
55+
56+
<div class="info-section">
57+
<div class="display-score">{{ player.points }} 分</div>
58+
<div class="nickname-container">
59+
<span class="display-nickname">{{ player.nickname }}</span>
60+
</div>
61+
62+
<div class="title-container">
63+
<select v-model="player.title" class="title-select">
64+
<option v-for="title in availableTitles" :key="title.id" :value="title.label">
65+
{{ title.label }}
66+
</option>
67+
</select>
68+
</div>
69+
</div>
70+
71+
<div class="scrollable-content">
72+
<div class="achievements-section">
73+
<h3>成就</h3>
74+
<div class="achievements-grid">
75+
<div v-for="medal in player.achievements" :key="medal.id" class="medal-item">
76+
<Icon :icon="medal.icon" class="medal-icon" :style="{ color: medal.unlocked ? '#F8C0C8' : '#888' }" />
77+
<span class="medal-label">{{ medal.unlocked ? medal.label : '???' }}</span>
78+
</div>
79+
</div>
80+
</div>
81+
</div>
82+
</div>
883
</template>
984

1085
<style scoped>
86+
#myProfile {
87+
width: 100%;
88+
height: 100%;
89+
background-color: #fbfaf2;
90+
display: flex;
91+
flex-direction: column;
92+
align-items: center;
93+
padding: 35px 20px 20px 20px;
94+
box-sizing: border-box;
95+
}
96+
97+
.avatar-section {
98+
display: flex;
99+
flex-direction: column;
100+
align-items: center;
101+
margin-bottom: 5px;
102+
flex-shrink: 0;
103+
}
104+
105+
.avatar-container {
106+
width: 150px;
107+
height: 150px;
108+
border-radius: 50%;
109+
background-color: #e0e6ec;
110+
display: flex;
111+
justify-content: center;
112+
align-items: center;
113+
overflow: hidden;
114+
border: 4px solid #fff;
115+
box-shadow: 0 0 0 2px #c9d2da;
116+
position: relative;
117+
}
118+
119+
.avatar-image {
120+
width: 100%;
121+
height: 90%;
122+
object-fit: cover;
123+
border-radius: 50%;
124+
transform: translateY(5px) translateX(3px);
125+
}
126+
127+
.info-section {
128+
width: 100%;
129+
display: flex;
130+
flex-direction: column;
131+
gap: 10px;
132+
margin-bottom: 20px;
133+
flex-shrink: 0;
134+
}
135+
136+
.display-score {
137+
font-size: 15px;
138+
font-family: 'Zen Maru Gothic', sans-serif;
139+
font-weight: bold;
140+
color: #4a4a4a;
141+
text-align: center;
142+
width: 100%;
143+
}
144+
145+
.nickname-container {
146+
display: flex;
147+
justify-content: center;
148+
align-items: center;
149+
height: 25px;
150+
}
151+
152+
.display-nickname {
153+
font-size: 20px;
154+
font-family: 'Zen Maru Gothic', sans-serif;
155+
font-weight: bold;
156+
color: #333;
157+
}
158+
159+
.title-container {
160+
background-color: #e6eef4;
161+
border-radius: 25px;
162+
padding: 10px 15px;
163+
display: flex;
164+
align-items: center;
165+
justify-content: center;
166+
}
167+
168+
.title-select {
169+
flex-grow: 1;
170+
border: none;
171+
background: transparent;
172+
outline: none;
173+
font-size: 16px;
174+
font-family: 'Zen Maru Gothic', sans-serif;
175+
color: #333;
176+
-webkit-appearance: none;
177+
-moz-appearance: none;
178+
appearance: none;
179+
cursor: pointer;
180+
text-align: center;
181+
}
182+
183+
.title-container::after {
184+
content: '';
185+
position: absolute;
186+
right: 15px;
187+
color: #888;
188+
font-size: 12px;
189+
pointer-events: none;
190+
}
191+
192+
.title-container {
193+
position: relative;
194+
}
195+
196+
.scrollable-content {
197+
flex-grow: 1;
198+
overflow-y: auto;
199+
width: 100%;
200+
padding-right: 5px;
201+
box-sizing: border-box;
202+
}
203+
204+
.achievements-section {
205+
padding: 0;
206+
box-sizing: border-box;
207+
text-align: center;
208+
position: relative;
209+
margin-top: 0;
210+
box-shadow: none;
211+
}
212+
213+
.achievements-section h3 {
214+
font-size: 20px;
215+
font-family: 'Zen Maru Gothic', sans-serif;
216+
font-weight: bold;
217+
color: #4A4A4A;
218+
margin-top: 0;
219+
margin-bottom: 15px;
220+
letter-spacing: 2px;
221+
}
222+
223+
.achievements-grid {
224+
display: grid;
225+
grid-template-columns: repeat(3, 1fr);
226+
gap: 15px;
227+
justify-items: center;
228+
margin-bottom: 70px;
229+
}
230+
231+
.medal-item {
232+
width: 80px;
233+
height: 80px;
234+
display: flex;
235+
flex-direction: column;
236+
justify-content: center;
237+
align-items: center;
238+
gap: 5px;
239+
}
240+
241+
.medal-icon {
242+
font-size: 48px;
243+
/* color: #F8C0C8; */
244+
}
245+
246+
.medal-label {
247+
font-size: 12px;
248+
font-family: 'Zen Maru Gothic', sans-serif;
249+
color: #555;
250+
text-align: center;
251+
white-space: nowrap;
252+
overflow: hidden;
253+
text-overflow: ellipsis;
254+
}
11255
256+
input:focus {
257+
outline: none;
258+
}
12259
</style>

src/components/PhaserGame.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type Phaser from 'phaser'
33
import { onMounted, onUnmounted, ref, watch, computed } from 'vue'
44
import { marked } from 'marked'
55
import { EventBus } from '../game/EventBus'
6-
import { GameData } from '../game/GameData'
6+
import { GameData } from '../data/GameData.ts'
77
import StartGame from '../game/main'
88
import { Icon } from '@iconify/vue'
99
import Danmaku from './Danmaku.vue'
@@ -268,7 +268,7 @@ watch([showPopup, popupData], async ([isOpen, data]) => {
268268
<div v-if="popupData?.type === 'Base'">
269269
<img
270270
alt="COSCUP x RubyConf Taiwan 2025 banner"
271-
src="../../public/assets/banner-mobile.png"
271+
src="/assets/banner-mobile.png"
272272
>
273273
</div>
274274
<div v-else-if="popupData?.type === 'Sponsor'" class="Sponsor">

src/components/QRCodeScanner.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ watch(cameraId, async (newCameraId) => {
4949
<template>
5050
<div>
5151
<div
52-
v-if="cameraId"
5352
:id="qrcodeRegionId"
5453
style="width: 300px; height: 300px;"
55-
></div>
56-
<div v-else>
57-
相機初始化中或權限未開啟...
54+
>
55+
<div v-if="!cameraId" style="text-align: center; padding-top: 50px;">
56+
相機初始化中或權限未開啟...
57+
</div>
5858
</div>
5959
</div>
6060
</template>

src/config/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import SponsorList from '../components/SponsorList.vue'
44
import QRCodeScanner from '../components/QRCodeScanner.vue'
55
import MyProfile from '../components/MyProfile.vue'
66

7+
const handleQrCodeScanned = (decodedText: string) => {
8+
console.log('QR Code 掃描成功:', decodedText)
9+
router.push({ path: '/', query: { scannedData: decodedText } });
10+
// 將結果存入 Pinia/Vuex store,供其他元件使用:
11+
// someStore.setScannedData(decodedText);
12+
alert(`掃描到的資料是:${decodedText}`);
13+
14+
}
15+
716
const routes: Array<RouteRecordRaw> = [
817
{
918
path: '/',
@@ -18,7 +27,10 @@ const routes: Array<RouteRecordRaw> = [
1827
{
1928
path: '/qrcode-scanner',
2029
name: 'qrcode-scanner',
21-
component: QRCodeScanner
30+
component: QRCodeScanner,
31+
props: (route) => ({
32+
qrCodeSuccessCallback: handleQrCodeScanned,
33+
}),
2234
},
2335
{
2436
path: '/profile',

src/data/AchievementsData.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export interface Medal {
2+
id: number
3+
icon: string
4+
label: string
5+
unlocked?: boolean
6+
}
7+
8+
export const achievements: Medal[] = [
9+
{ id: 1, icon: 'mdi:medal', label: '只是路過', unlocked: false }, // 聊天室發 1 則留言
10+
{ id: 2, icon: 'ph:chat-centered-text', label: '開始融入', unlocked: false }, // 聊天室發 10 則留言
11+
{ id: 3, icon: 'mdi:trophy-award', label: '意見領袖', unlocked: false }, // 聊天室發 30 則留言
12+
13+
{ id: 4, icon: 'mdi:heart-outline', label: '有人理我', unlocked: false }, // 聊天室留言獲得 1 個愛心
14+
{ id: 5, icon: 'mdi:heart-multiple-outline', label: '人氣新星', unlocked: false }, // 聊天室留言獲得 30 個愛心
15+
{ id: 6, icon: 'mdi:heart-circle', label: '魅力無法擋', unlocked: false }, // 聊天室留言獲得 100 個愛心
16+
17+
{ id: 7, icon: 'mdi:hand-heart-outline', label: '給你一顆愛心', unlocked: false }, // 按別人留言愛心 1 次
18+
{ id: 8, icon: 'mdi:hand-coin-outline', label: '暖心使者', unlocked: false }, // 按別人留言愛心 10 次
19+
{ id: 9, icon: 'mdi:cards-heart', label: '真心不騙', unlocked: false }, // 按別人留言愛心 30 次
20+
21+
{ id: 10, icon: 'fluent:trophy-16-filled', label: '早起的鳥兒有蟲吃', unlocked: true }, // 參與第一天或第二天的開幕
22+
{ id: 11, icon: 'mdi:flag-checkered', label: '最後一哩路', unlocked: false }, // 參與第一天的閉幕
23+
{ id: 12, icon: 'material-symbols:star', label: '有始有終', unlocked: false }, // 參與開幕與閉幕
24+
// 攤位數量需要確認下
25+
{ id: 13, icon: 'mdi:crown', label: '第一哩路', unlocked: true }, // 獲得攤位 1 個板塊
26+
{ id: 14, icon: 'mdi:map-marker-radius-outline', label: '開疆闢土', unlocked: false }, // 獲得攤位 5 個板塊
27+
{ id: 15, icon: 'mdi:earth-box', label: '吾土吾疆', unlocked: false }, // 獲得攤位 30 個板塊
28+
29+
// 有去 TR
30+
// 有去 RB
31+
// 有去 AU
32+
33+
]
File renamed without changes.

src/game/TileData.ts renamed to src/data/TileData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Phaser from 'phaser'
2-
import { EventBus } from './EventBus'
2+
import { EventBus } from '../game/EventBus'
33
import { GameData } from './GameData'
44

55
function hexToHSL(hex: number): { h: number; s: number; l: number } {

0 commit comments

Comments
 (0)