@kennofizet/rewardplay-frontend 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -40
- package/package.json +1 -1
- package/src/api/index.js +142 -4
- package/src/components/BonusSection.vue +256 -0
- package/src/components/CustomOptionDisplay.vue +77 -0
- package/src/components/CustomSelect.vue +220 -0
- package/src/components/LoadingSource.vue +72 -9
- package/src/components/LoginScreen.vue +119 -13
- package/src/components/MainGame.vue +40 -11
- package/src/components/StatMapPreview.vue +294 -0
- package/src/components/game/AlertMessage.vue +153 -0
- package/src/components/game/BackgroundLuckyWheel.vue +8089 -0
- package/src/components/game/EventsPopup.vue +408 -0
- package/src/components/game/ExpBar.vue +140 -0
- package/src/components/game/GameLayout.vue +33 -9
- package/src/components/game/ItemBox.vue +43 -19
- package/src/components/game/LevelBadge.vue +112 -0
- package/src/components/game/MenuItem.vue +10 -9
- package/src/components/game/RankingItem.vue +58 -17
- package/src/components/game/RewardCard.vue +84 -20
- package/src/components/game/RewardItem.vue +43 -3
- package/src/components/game/RewardItemsTooltip.vue +217 -0
- package/src/components/game/Sector.vue +15 -2
- package/src/components/game/TopCoinCard.vue +178 -0
- package/src/components/game/TopCoinCardSkeleton.vue +133 -0
- package/src/components/game/TopMeCard.vue +121 -0
- package/src/components/game/TopMeCardSkeleton.vue +92 -0
- package/src/components/game/TopMenu.vue +23 -11
- package/src/components/game/TopWeekCard.vue +333 -0
- package/src/components/game/TopWeekCardSkeleton.vue +143 -0
- package/src/components/game/WeekDay.vue +1 -1
- package/src/components/game/ZoneSelectModal.vue +76 -0
- package/src/components/game/ZoneSelectPage.vue +64 -0
- package/src/components/ui/ErrorState.vue +69 -0
- package/src/composables/useResourceLoader.js +1 -69
- package/src/composables/useTimezone.js +100 -0
- package/src/data/eventsDemo.js +63 -0
- package/src/data/shopDemo.js +160 -0
- package/src/i18n/index.js +40 -0
- package/src/i18n/translations/en.js +789 -0
- package/src/i18n/translations/vi.js +792 -0
- package/src/i18n/utils.js +40 -0
- package/src/index.js +25 -0
- package/src/pages/ComingSoonPage.vue +371 -0
- package/src/pages/RewardPlayPage.vue +425 -49
- package/src/pages/game/BagGearPage.vue +1805 -117
- package/src/pages/game/DailyRewardPage.vue +251 -116
- package/src/pages/game/LuckyWheelPage.vue +4 -546
- package/src/pages/game/ManageSettingPage.vue +195 -0
- package/src/pages/game/RankingPage.vue +214 -0
- package/src/pages/game/RulesPage.vue +26 -13
- package/src/pages/game/ShopPage.vue +812 -11
- package/src/pages/game/manage-setting/ManageZonesPage.vue +579 -0
- package/src/pages/game/manage-setting/SettingBoxTicketBuffPage.vue +502 -0
- package/src/pages/game/manage-setting/SettingDailyRewardConfigPage.vue +288 -0
- package/src/pages/game/manage-setting/SettingEventsListPage.vue +836 -0
- package/src/pages/game/manage-setting/SettingItemSetsListPage.vue +1132 -0
- package/src/pages/game/manage-setting/SettingItemsListPage.vue +1134 -0
- package/src/pages/game/manage-setting/SettingLevelExpListPage.vue +242 -0
- package/src/pages/game/manage-setting/SettingOptionsListPage.vue +775 -0
- package/src/pages/game/manage-setting/SettingShopListPage.vue +851 -0
- package/src/pages/game/manage-setting/SettingStackBonusListPage.vue +280 -0
- package/src/pages/game/manage-setting/SettingStatsTransformListPage.vue +853 -0
- package/src/utils/constants.js +141 -0
- package/src/utils/globalData.js +174 -0
- package/src/utils/imageResolverRuntime.js +92 -0
- package/src/utils/numberFormat.js +17 -0
- package/src/utils/resourceLoader.js +228 -52
- package/src/utils/settingApiResponse.js +35 -0
- package/src/utils/statHelpers.js +144 -0
- package/src/utils/timezone.js +123 -0
- package/src/pages/game/RankingCoinPage.vue +0 -184
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="custom-select" :class="{ 'is-open': isOpen, 'is-disabled': disabled }" ref="dropdownRef">
|
|
3
|
+
<div
|
|
4
|
+
class="select-trigger"
|
|
5
|
+
@click="toggleDropdown"
|
|
6
|
+
:class="triggerClass"
|
|
7
|
+
>
|
|
8
|
+
<span class="select-value">{{ displayValue }}</span>
|
|
9
|
+
<span class="select-arrow" :class="{ 'rotated': isOpen }">▼</span>
|
|
10
|
+
</div>
|
|
11
|
+
<Transition name="dropdown">
|
|
12
|
+
<div v-if="isOpen" class="select-dropdown">
|
|
13
|
+
<div
|
|
14
|
+
v-for="option in options"
|
|
15
|
+
:key="option.value"
|
|
16
|
+
class="select-option"
|
|
17
|
+
:class="{ 'is-selected': modelValue === option.value, 'is-disabled': option.disabled }"
|
|
18
|
+
@click="selectOption(option)"
|
|
19
|
+
>
|
|
20
|
+
{{ option.label }}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</Transition>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup>
|
|
28
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
29
|
+
|
|
30
|
+
const props = defineProps({
|
|
31
|
+
modelValue: {
|
|
32
|
+
type: [String, Number],
|
|
33
|
+
default: ''
|
|
34
|
+
},
|
|
35
|
+
options: {
|
|
36
|
+
type: Array,
|
|
37
|
+
required: true,
|
|
38
|
+
validator: (options) => {
|
|
39
|
+
return options.every(opt => opt.hasOwnProperty('value') && opt.hasOwnProperty('label'))
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
placeholder: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: 'Select...'
|
|
45
|
+
},
|
|
46
|
+
disabled: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: false
|
|
49
|
+
},
|
|
50
|
+
triggerClass: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: ''
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits(['update:modelValue', 'change'])
|
|
57
|
+
|
|
58
|
+
const isOpen = ref(false)
|
|
59
|
+
const dropdownRef = ref(null)
|
|
60
|
+
|
|
61
|
+
const displayValue = computed(() => {
|
|
62
|
+
if (!props.modelValue) {
|
|
63
|
+
return props.placeholder
|
|
64
|
+
}
|
|
65
|
+
const selectedOption = props.options.find(opt => opt.value === props.modelValue)
|
|
66
|
+
return selectedOption ? selectedOption.label : props.placeholder
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const toggleDropdown = () => {
|
|
70
|
+
if (props.disabled) return
|
|
71
|
+
isOpen.value = !isOpen.value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const selectOption = (option) => {
|
|
75
|
+
if (option.disabled) return
|
|
76
|
+
emit('update:modelValue', option.value)
|
|
77
|
+
emit('change', option.value)
|
|
78
|
+
isOpen.value = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handleClickOutside = (event) => {
|
|
82
|
+
if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
|
|
83
|
+
isOpen.value = false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onMounted(() => {
|
|
88
|
+
document.addEventListener('click', handleClickOutside)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
onUnmounted(() => {
|
|
92
|
+
document.removeEventListener('click', handleClickOutside)
|
|
93
|
+
})
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<style scoped>
|
|
97
|
+
.custom-select {
|
|
98
|
+
position: relative;
|
|
99
|
+
display: inline-block;
|
|
100
|
+
max-width: 100%;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.select-trigger {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
padding: 8px 12px;
|
|
108
|
+
background: rgba(255, 255, 255, 0.1);
|
|
109
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
color: #fff;
|
|
112
|
+
font-size: 0.9rem;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
transition: all 0.2s ease;
|
|
115
|
+
user-select: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.custom-select.is-disabled .select-trigger {
|
|
119
|
+
opacity: 0.6;
|
|
120
|
+
cursor: not-allowed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.select-trigger:hover:not(.is-disabled) {
|
|
124
|
+
background: rgba(255, 255, 255, 0.15);
|
|
125
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.custom-select.is-open .select-trigger {
|
|
129
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.select-value {
|
|
133
|
+
flex: 1;
|
|
134
|
+
text-align: left;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
text-overflow: ellipsis;
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.select-arrow {
|
|
141
|
+
margin-left: 8px;
|
|
142
|
+
font-size: 0.7rem;
|
|
143
|
+
color: rgba(255, 255, 255, 0.8);
|
|
144
|
+
transition: transform 0.2s ease;
|
|
145
|
+
flex-shrink: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.select-arrow.rotated {
|
|
149
|
+
transform: rotate(180deg);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.select-dropdown {
|
|
153
|
+
position: absolute;
|
|
154
|
+
top: calc(100% + 4px);
|
|
155
|
+
left: 0;
|
|
156
|
+
right: 0;
|
|
157
|
+
background: #253344;
|
|
158
|
+
border: 1px solid #1a2332;
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
161
|
+
z-index: 1000;
|
|
162
|
+
max-height: 200px;
|
|
163
|
+
overflow-y: auto;
|
|
164
|
+
margin-top: 4px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.select-option {
|
|
168
|
+
padding: 10px 12px;
|
|
169
|
+
color: #d0d4d6;
|
|
170
|
+
font-size: 0.9rem;
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
transition: background-color 0.2s ease;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.select-option:hover:not(.is-disabled) {
|
|
176
|
+
background: rgba(255, 255, 255, 0.1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.select-option.is-selected {
|
|
180
|
+
background: rgba(246, 169, 1, 0.2);
|
|
181
|
+
color: #f6a901;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.select-option.is-disabled {
|
|
186
|
+
opacity: 0.5;
|
|
187
|
+
cursor: not-allowed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Dropdown transition */
|
|
191
|
+
.dropdown-enter-active,
|
|
192
|
+
.dropdown-leave-active {
|
|
193
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.dropdown-enter-from,
|
|
197
|
+
.dropdown-leave-to {
|
|
198
|
+
opacity: 0;
|
|
199
|
+
transform: translateY(-10px);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Scrollbar styling */
|
|
203
|
+
.select-dropdown::-webkit-scrollbar {
|
|
204
|
+
width: 6px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.select-dropdown::-webkit-scrollbar-track {
|
|
208
|
+
background: #1a2332;
|
|
209
|
+
border-radius: 3px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.select-dropdown::-webkit-scrollbar-thumb {
|
|
213
|
+
background: #253344;
|
|
214
|
+
border-radius: 3px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.select-dropdown::-webkit-scrollbar-thumb:hover {
|
|
218
|
+
background: #2d3a4b;
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
@@ -37,9 +37,37 @@ const props = defineProps({
|
|
|
37
37
|
type: Number,
|
|
38
38
|
default: 0
|
|
39
39
|
},
|
|
40
|
+
userDataProgress: {
|
|
41
|
+
type: Number,
|
|
42
|
+
default: 0
|
|
43
|
+
},
|
|
40
44
|
backgroundImage: {
|
|
41
45
|
type: String,
|
|
42
46
|
default: null
|
|
47
|
+
},
|
|
48
|
+
loadingTitle: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: 'LOADING'
|
|
51
|
+
},
|
|
52
|
+
loadingSubtitle: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: 'Preparing your gaming experience...'
|
|
55
|
+
},
|
|
56
|
+
loadingLabels: {
|
|
57
|
+
type: Object,
|
|
58
|
+
default: () => ({
|
|
59
|
+
assets: 'Loading Assets',
|
|
60
|
+
unzipping: 'Unzipping Files',
|
|
61
|
+
userData: 'Load Data User'
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
loadingSubTexts: {
|
|
65
|
+
type: Object,
|
|
66
|
+
default: () => ({
|
|
67
|
+
assets: '',
|
|
68
|
+
unzipping: '',
|
|
69
|
+
userData: ''
|
|
70
|
+
})
|
|
43
71
|
}
|
|
44
72
|
})
|
|
45
73
|
|
|
@@ -155,7 +183,8 @@ const updateCanvas = () => {
|
|
|
155
183
|
ctx.font = 'bold 48px Arial, sans-serif'
|
|
156
184
|
ctx.textAlign = 'center'
|
|
157
185
|
ctx.textBaseline = 'middle'
|
|
158
|
-
|
|
186
|
+
const loadingTitle = props.loadingTitle
|
|
187
|
+
ctx.fillText(loadingTitle, width / 2, titleY)
|
|
159
188
|
|
|
160
189
|
// Reset shadow
|
|
161
190
|
ctx.shadowBlur = 0
|
|
@@ -163,11 +192,12 @@ const updateCanvas = () => {
|
|
|
163
192
|
// Draw subtitle
|
|
164
193
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
|
|
165
194
|
ctx.font = '18px Arial, sans-serif'
|
|
166
|
-
|
|
195
|
+
const loadingSubtitle = props.loadingSubtitle
|
|
196
|
+
ctx.fillText(loadingSubtitle, width / 2, titleY + 50)
|
|
167
197
|
|
|
168
198
|
// Draw main loading bar with chunky game-like style
|
|
169
|
-
// Calculate total progress as average of Loading Assets and
|
|
170
|
-
const totalProgress = (props.loadingProgress + props.unzipProgress) /
|
|
199
|
+
// Calculate total progress as average of Loading Assets, Unzipping Files, and Load Data User
|
|
200
|
+
const totalProgress = (props.loadingProgress + props.unzipProgress + props.userDataProgress) / 3
|
|
171
201
|
const barWidth = width * 0.65
|
|
172
202
|
const barHeight = 35
|
|
173
203
|
const barX = (width - barWidth) / 2
|
|
@@ -262,12 +292,25 @@ const updateCanvas = () => {
|
|
|
262
292
|
|
|
263
293
|
// Draw progress items with better styling
|
|
264
294
|
const progressItems = [
|
|
265
|
-
{
|
|
266
|
-
|
|
295
|
+
{
|
|
296
|
+
label: props.loadingLabels.assets,
|
|
297
|
+
progress: Math.min(props.loadingProgress, 100),
|
|
298
|
+
subText: props.loadingSubTexts.assets || ''
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
label: props.loadingLabels.unzipping,
|
|
302
|
+
progress: Math.min(props.unzipProgress, 100),
|
|
303
|
+
subText: props.loadingSubTexts.unzipping || ''
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
label: props.loadingLabels.userData,
|
|
307
|
+
progress: Math.min(props.userDataProgress, 100),
|
|
308
|
+
subText: props.loadingSubTexts.userData || ''
|
|
309
|
+
}
|
|
267
310
|
]
|
|
268
311
|
|
|
269
312
|
const startY = height * 0.68
|
|
270
|
-
const itemHeight =
|
|
313
|
+
const itemHeight = 40 // Increased to accommodate sub-text
|
|
271
314
|
const spacing = 12
|
|
272
315
|
|
|
273
316
|
progressItems.forEach((item, index) => {
|
|
@@ -300,10 +343,30 @@ const updateCanvas = () => {
|
|
|
300
343
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'
|
|
301
344
|
ctx.textAlign = 'right'
|
|
302
345
|
ctx.fillText(`${Math.round(item.progress)}%`, itemBarX + itemBarWidth, y - 5)
|
|
346
|
+
|
|
347
|
+
// Sub-text under progress bar
|
|
348
|
+
if (item.subText) {
|
|
349
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
|
|
350
|
+
ctx.font = '12px Arial, sans-serif'
|
|
351
|
+
ctx.textAlign = 'left'
|
|
352
|
+
ctx.textBaseline = 'top'
|
|
353
|
+
// Truncate long text
|
|
354
|
+
const maxWidth = itemBarWidth - 10
|
|
355
|
+
let displayText = item.subText
|
|
356
|
+
const metrics = ctx.measureText(displayText)
|
|
357
|
+
if (metrics.width > maxWidth) {
|
|
358
|
+
// Truncate with ellipsis
|
|
359
|
+
while (ctx.measureText(displayText + '...').width > maxWidth && displayText.length > 0) {
|
|
360
|
+
displayText = displayText.slice(0, -1)
|
|
361
|
+
}
|
|
362
|
+
displayText = displayText + '...'
|
|
363
|
+
}
|
|
364
|
+
ctx.fillText(displayText, itemBarX, y + 12)
|
|
365
|
+
}
|
|
303
366
|
})
|
|
304
367
|
|
|
305
368
|
// Check if loading is complete
|
|
306
|
-
if (props.loadingProgress >= 100 && props.unzipProgress >= 100) {
|
|
369
|
+
if (props.loadingProgress >= 100 && props.unzipProgress >= 100 && props.userDataProgress >= 100) {
|
|
307
370
|
emit('loading-complete')
|
|
308
371
|
}
|
|
309
372
|
}
|
|
@@ -325,7 +388,7 @@ const handleResize = () => {
|
|
|
325
388
|
})
|
|
326
389
|
}
|
|
327
390
|
|
|
328
|
-
watch([() => props.isLoading, () => props.loadingProgress, () => props.unzipProgress], () => {
|
|
391
|
+
watch([() => props.isLoading, () => props.loadingProgress, () => props.unzipProgress, () => props.userDataProgress], () => {
|
|
329
392
|
if (props.isLoading) {
|
|
330
393
|
nextTick(() => {
|
|
331
394
|
if (animationFrameId) {
|
|
@@ -30,7 +30,8 @@ const emit = defineEmits(['login-success', 'login-failed'])
|
|
|
30
30
|
const gameApi = inject('gameApi')
|
|
31
31
|
const canvasRef = ref(null)
|
|
32
32
|
const error = ref(null)
|
|
33
|
-
const errorType = ref(null) // 'no-api', 'invalid-token', 'connection-failed', 'server-error'
|
|
33
|
+
const errorType = ref(null) // 'no-api', 'invalid-token', 'connection-failed', 'server-error', 'no-zone'
|
|
34
|
+
const errorMessage = ref(null) // Custom error message to display
|
|
34
35
|
const status = ref('checking') // 'checking', 'success', 'error'
|
|
35
36
|
let animationFrameId = null
|
|
36
37
|
let pulseAnimation = 0
|
|
@@ -67,6 +68,10 @@ const drawContent = (ctx, width, height) => {
|
|
|
67
68
|
drawCheckingAnimation(ctx, centerX, centerY, width, height)
|
|
68
69
|
} else if (status.value === 'error') {
|
|
69
70
|
drawErrorIcon(ctx, centerX, centerY, width, height)
|
|
71
|
+
// Draw error message if available
|
|
72
|
+
if (errorMessage.value) {
|
|
73
|
+
drawErrorMessage(ctx, centerX, centerY + Math.min(width, height) * 0.15, width, height)
|
|
74
|
+
}
|
|
70
75
|
} else if (status.value === 'success') {
|
|
71
76
|
drawSuccessIcon(ctx, centerX, centerY, width, height)
|
|
72
77
|
}
|
|
@@ -172,6 +177,8 @@ const drawErrorIcon = (ctx, centerX, centerY, width, height) => {
|
|
|
172
177
|
drawConnectionError(ctx, centerX, centerY + size * 1.2, width, height)
|
|
173
178
|
} else if (errorType.value === 'server-error') {
|
|
174
179
|
drawServerError(ctx, centerX, centerY + size * 1.2, width, height)
|
|
180
|
+
} else if (errorType.value === 'no-zone') {
|
|
181
|
+
drawNoZoneError(ctx, centerX, centerY + size * 1.2, width, height)
|
|
175
182
|
} else {
|
|
176
183
|
drawGenericError(ctx, centerX, centerY + size * 1.2, width, height)
|
|
177
184
|
}
|
|
@@ -426,6 +433,91 @@ const drawGenericError = (ctx, centerX, centerY, width, height) => {
|
|
|
426
433
|
}
|
|
427
434
|
}
|
|
428
435
|
|
|
436
|
+
const drawNoZoneError = (ctx, centerX, centerY, width, height) => {
|
|
437
|
+
const iconSize = Math.min(width, height) * 0.08
|
|
438
|
+
|
|
439
|
+
// Draw zone/server icon with X mark
|
|
440
|
+
ctx.strokeStyle = '#ff6666'
|
|
441
|
+
ctx.fillStyle = '#ff6666'
|
|
442
|
+
ctx.lineWidth = iconSize * 0.12
|
|
443
|
+
ctx.lineCap = 'round'
|
|
444
|
+
|
|
445
|
+
// Draw server/zone box
|
|
446
|
+
const boxX = centerX - iconSize * 0.4
|
|
447
|
+
const boxY = centerY - iconSize * 0.3
|
|
448
|
+
const boxWidth = iconSize * 0.8
|
|
449
|
+
const boxHeight = iconSize * 0.6
|
|
450
|
+
|
|
451
|
+
ctx.strokeRect(boxX, boxY, boxWidth, boxHeight)
|
|
452
|
+
|
|
453
|
+
// Draw X mark over the box
|
|
454
|
+
ctx.strokeStyle = '#ff0000'
|
|
455
|
+
ctx.lineWidth = iconSize * 0.15
|
|
456
|
+
const xSize = iconSize * 0.5
|
|
457
|
+
|
|
458
|
+
ctx.beginPath()
|
|
459
|
+
ctx.moveTo(boxX + boxWidth * 0.2, boxY + boxHeight * 0.2)
|
|
460
|
+
ctx.lineTo(boxX + boxWidth * 0.8, boxY + boxHeight * 0.8)
|
|
461
|
+
ctx.stroke()
|
|
462
|
+
|
|
463
|
+
ctx.beginPath()
|
|
464
|
+
ctx.moveTo(boxX + boxWidth * 0.8, boxY + boxHeight * 0.2)
|
|
465
|
+
ctx.lineTo(boxX + boxWidth * 0.2, boxY + boxHeight * 0.8)
|
|
466
|
+
ctx.stroke()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const drawErrorMessage = (ctx, centerX, centerY, width, height) => {
|
|
470
|
+
if (!errorMessage.value) return
|
|
471
|
+
|
|
472
|
+
ctx.save()
|
|
473
|
+
ctx.fillStyle = '#ffffff'
|
|
474
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'
|
|
475
|
+
ctx.lineWidth = 2
|
|
476
|
+
ctx.textAlign = 'center'
|
|
477
|
+
ctx.textBaseline = 'middle'
|
|
478
|
+
|
|
479
|
+
// Calculate font size based on screen size
|
|
480
|
+
const fontSize = Math.min(width, height) * 0.025
|
|
481
|
+
ctx.font = `bold ${fontSize}px Arial, sans-serif`
|
|
482
|
+
|
|
483
|
+
// Draw text with outline for better visibility
|
|
484
|
+
const text = errorMessage.value
|
|
485
|
+
const maxWidth = width * 0.8
|
|
486
|
+
const words = text.split(' ')
|
|
487
|
+
const lines = []
|
|
488
|
+
let currentLine = ''
|
|
489
|
+
|
|
490
|
+
// Word wrap
|
|
491
|
+
words.forEach(word => {
|
|
492
|
+
const testLine = currentLine + (currentLine ? ' ' : '') + word
|
|
493
|
+
const metrics = ctx.measureText(testLine)
|
|
494
|
+
if (metrics.width > maxWidth && currentLine) {
|
|
495
|
+
lines.push(currentLine)
|
|
496
|
+
currentLine = word
|
|
497
|
+
} else {
|
|
498
|
+
currentLine = testLine
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
if (currentLine) {
|
|
502
|
+
lines.push(currentLine)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Draw each line
|
|
506
|
+
const lineHeight = fontSize * 1.4
|
|
507
|
+
const startY = centerY - ((lines.length - 1) * lineHeight) / 2
|
|
508
|
+
|
|
509
|
+
lines.forEach((line, index) => {
|
|
510
|
+
const y = startY + index * lineHeight
|
|
511
|
+
|
|
512
|
+
// Draw text outline (shadow)
|
|
513
|
+
ctx.strokeText(line, centerX, y)
|
|
514
|
+
// Draw text
|
|
515
|
+
ctx.fillText(line, centerX, y)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
ctx.restore()
|
|
519
|
+
}
|
|
520
|
+
|
|
429
521
|
const drawSuccessIcon = (ctx, centerX, centerY, width, height) => {
|
|
430
522
|
const size = Math.min(width, height) * 0.15
|
|
431
523
|
const x = centerX
|
|
@@ -498,11 +590,14 @@ const animate = () => {
|
|
|
498
590
|
}
|
|
499
591
|
}
|
|
500
592
|
|
|
593
|
+
const translator = inject('translator', null)
|
|
594
|
+
const t = translator || ((key) => key)
|
|
595
|
+
|
|
501
596
|
const checkUser = async () => {
|
|
502
597
|
if (!gameApi) {
|
|
503
598
|
status.value = 'error'
|
|
504
599
|
errorType.value = 'no-api'
|
|
505
|
-
emit('login-failed', '
|
|
600
|
+
emit('login-failed', t('component.login.apiNotAvailable'))
|
|
506
601
|
return
|
|
507
602
|
}
|
|
508
603
|
|
|
@@ -514,16 +609,15 @@ const checkUser = async () => {
|
|
|
514
609
|
|
|
515
610
|
if (response.data.success) {
|
|
516
611
|
status.value = 'success'
|
|
612
|
+
// Determine is_manager flag from response (if present)
|
|
613
|
+
const isManager = !!(response.data?.datas?.is_manager)
|
|
517
614
|
// Wait a moment to show success icon
|
|
518
615
|
setTimeout(() => {
|
|
519
|
-
emit('login-success', {
|
|
520
|
-
...response.data.user,
|
|
521
|
-
imagesUrl: response.data.images_url || ''
|
|
522
|
-
})
|
|
616
|
+
emit('login-success', { is_manager: isManager })
|
|
523
617
|
}, 500)
|
|
524
618
|
} else {
|
|
525
619
|
status.value = 'error'
|
|
526
|
-
const errorMsg = response.data.
|
|
620
|
+
const errorMsg = response.data.message || t('component.login.authenticationFailed')
|
|
527
621
|
|
|
528
622
|
// Determine error type
|
|
529
623
|
if (errorMsg.toLowerCase().includes('token') || errorMsg.toLowerCase().includes('invalid')) {
|
|
@@ -538,20 +632,32 @@ const checkUser = async () => {
|
|
|
538
632
|
}
|
|
539
633
|
} catch (err) {
|
|
540
634
|
status.value = 'error'
|
|
541
|
-
const
|
|
635
|
+
const errorMsg = err.response?.data?.message || err.message || t('component.login.connectionFailed')
|
|
636
|
+
|
|
637
|
+
// Handle 403 error specifically
|
|
638
|
+
if (err.response?.status === 403) {
|
|
639
|
+
errorType.value = 'no-zone'
|
|
640
|
+
errorMessage.value = 'User not in any zone or not managing any server'
|
|
641
|
+
emit('login-failed', errorMessage.value)
|
|
642
|
+
return
|
|
643
|
+
}
|
|
542
644
|
|
|
543
|
-
// Determine error type
|
|
544
|
-
if (err.code === 'ECONNREFUSED' || err.message.includes('Network') ||
|
|
645
|
+
// Determine error type for other errors
|
|
646
|
+
if (err.code === 'ECONNREFUSED' || err.message.includes('Network') || errorMsg.includes('Connection')) {
|
|
545
647
|
errorType.value = 'connection-failed'
|
|
648
|
+
errorMessage.value = null
|
|
546
649
|
} else if (err.response?.status >= 500) {
|
|
547
650
|
errorType.value = 'server-error'
|
|
548
|
-
|
|
651
|
+
errorMessage.value = null
|
|
652
|
+
} else if (err.response?.status === 401 || errorMsg.includes('token') || errorMsg.includes('Invalid')) {
|
|
549
653
|
errorType.value = 'invalid-token'
|
|
654
|
+
errorMessage.value = null
|
|
550
655
|
} else {
|
|
551
656
|
errorType.value = 'generic'
|
|
657
|
+
errorMessage.value = null
|
|
552
658
|
}
|
|
553
659
|
|
|
554
|
-
emit('login-failed',
|
|
660
|
+
emit('login-failed', errorMsg)
|
|
555
661
|
}
|
|
556
662
|
}
|
|
557
663
|
|
|
@@ -564,6 +670,7 @@ watch(() => props.showLogin, (newVal) => {
|
|
|
564
670
|
status.value = 'checking'
|
|
565
671
|
error.value = null
|
|
566
672
|
errorType.value = null
|
|
673
|
+
errorMessage.value = null
|
|
567
674
|
pulseAnimation = 0
|
|
568
675
|
errorAnimation = 0
|
|
569
676
|
animate()
|
|
@@ -583,7 +690,6 @@ onMounted(() => {
|
|
|
583
690
|
if (props.showLogin) {
|
|
584
691
|
nextTick(() => {
|
|
585
692
|
animate()
|
|
586
|
-
checkUser()
|
|
587
693
|
})
|
|
588
694
|
}
|
|
589
695
|
})
|
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<GameLayout
|
|
3
3
|
:rotate="rotate"
|
|
4
|
+
:is-manager="isManager"
|
|
4
5
|
@page-change="handlePageChange"
|
|
5
6
|
@icon-click="handleIconClick"
|
|
6
7
|
>
|
|
7
8
|
<component :is="currentPage" />
|
|
9
|
+
<template #overlay>
|
|
10
|
+
<EventsPopup
|
|
11
|
+
v-if="showEventsPopup && activeEvents.length > 0"
|
|
12
|
+
:events="activeEvents"
|
|
13
|
+
@close="showEventsPopup = false"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
8
16
|
</GameLayout>
|
|
9
17
|
</template>
|
|
10
18
|
|
|
11
19
|
<script setup>
|
|
12
|
-
import { ref, shallowRef } from 'vue'
|
|
20
|
+
import { ref, shallowRef, onMounted, inject } from 'vue'
|
|
13
21
|
import GameLayout from './game/GameLayout.vue'
|
|
22
|
+
import EventsPopup from './game/EventsPopup.vue'
|
|
14
23
|
import DailyRewardPage from '../pages/game/DailyRewardPage.vue'
|
|
15
24
|
import BagGearPage from '../pages/game/BagGearPage.vue'
|
|
16
25
|
import LuckyWheelPage from '../pages/game/LuckyWheelPage.vue'
|
|
17
|
-
import
|
|
26
|
+
import RankingPage from '../pages/game/RankingPage.vue'
|
|
18
27
|
import RulesPage from '../pages/game/RulesPage.vue'
|
|
28
|
+
import ShopPage from '../pages/game/ShopPage.vue'
|
|
29
|
+
import ComingSoonPage from '../pages/ComingSoonPage.vue'
|
|
30
|
+
import ManageSettingPage from '../pages/game/ManageSettingPage.vue'
|
|
19
31
|
|
|
20
32
|
const props = defineProps({
|
|
21
|
-
imagesUrl: {
|
|
22
|
-
type: String,
|
|
23
|
-
default: ''
|
|
24
|
-
},
|
|
25
33
|
rotate: {
|
|
26
34
|
type: Boolean,
|
|
27
35
|
default: true
|
|
36
|
+
},
|
|
37
|
+
isManager: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: false
|
|
28
40
|
}
|
|
29
41
|
})
|
|
30
42
|
|
|
@@ -32,19 +44,37 @@ const pageMap = {
|
|
|
32
44
|
'reward': DailyRewardPage,
|
|
33
45
|
'bag': BagGearPage,
|
|
34
46
|
'lucky-wheel': LuckyWheelPage,
|
|
35
|
-
'ranking':
|
|
36
|
-
'rules':
|
|
37
|
-
'shop':
|
|
47
|
+
'ranking': RankingPage,
|
|
48
|
+
'rules': ComingSoonPage,
|
|
49
|
+
'shop': ShopPage,
|
|
50
|
+
'manage-setting': ManageSettingPage
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
const currentPage = shallowRef(DailyRewardPage) // Default to DailyReward
|
|
54
|
+
const showEventsPopup = ref(false)
|
|
55
|
+
const activeEvents = ref([])
|
|
56
|
+
const gameApi = inject('gameApi', null)
|
|
57
|
+
|
|
58
|
+
onMounted(async () => {
|
|
59
|
+
if (gameApi) {
|
|
60
|
+
try {
|
|
61
|
+
const res = await gameApi.getPlayerEvents()
|
|
62
|
+
const list = res?.data?.datas?.events ?? []
|
|
63
|
+
activeEvents.value = Array.isArray(list) ? list : []
|
|
64
|
+
if (activeEvents.value.length > 0) {
|
|
65
|
+
showEventsPopup.value = true
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
activeEvents.value = []
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
41
72
|
|
|
42
73
|
const handlePageChange = (page) => {
|
|
43
74
|
if (page === 'back') {
|
|
44
75
|
// Handle back navigation
|
|
45
76
|
return
|
|
46
77
|
}
|
|
47
|
-
|
|
48
78
|
if (pageMap[page]) {
|
|
49
79
|
const pageComponent = pageMap[page]
|
|
50
80
|
if (typeof pageComponent === 'function') {
|
|
@@ -60,7 +90,6 @@ const handlePageChange = (page) => {
|
|
|
60
90
|
|
|
61
91
|
const handleIconClick = (icon) => {
|
|
62
92
|
// Handle icon clicks (notifications, settings, etc.)
|
|
63
|
-
console.log('Icon clicked:', icon)
|
|
64
93
|
}
|
|
65
94
|
</script>
|
|
66
95
|
|