@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
package/README.md
CHANGED
|
@@ -1,63 +1,131 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kennofizet/rewardplay-frontend
|
|
2
2
|
|
|
3
|
-
Vue
|
|
3
|
+
Vue 3 frontend package for **RewardPlay**: game UI (bag, shop, daily reward, ranking, settings) that talks to the RewardPlay backend API.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What This Package Is
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
- **Vue 3 component library** for the RewardPlay game interface (login, loading, bag/gear, shop, daily rewards, ranking, manage settings).
|
|
8
|
+
- **Token-based**: you pass a backend URL and token; the package uses them for all API calls.
|
|
9
|
+
- **Embeddable**: mount it in a host app inside a div (e.g. after login or when entering the game section).
|
|
10
|
+
- Backend API, auth, and data format are defined in **@kennofizet/rewardplay-backend**. Use that package (or its docs) for token, endpoints, and data structures.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
cd ../.. # Go to monorepo root
|
|
12
|
-
npm install
|
|
13
|
-
```
|
|
12
|
+
## Installation
|
|
14
13
|
|
|
15
|
-
### From Package Directory
|
|
16
14
|
```bash
|
|
17
|
-
npm
|
|
15
|
+
npm i @kennofizet/rewardplay-frontend
|
|
18
16
|
```
|
|
19
|
-
This will install dependencies, but they'll be hoisted to the root `node_modules/` (this is normal for workspaces).
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
**Peer dependency:** Vue 3 (`vue@^3.2.0`). The host project must provide Vue.
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
## Setup: Mount Point and Initialization
|
|
24
21
|
|
|
25
|
-
1.
|
|
26
|
-
2. ✅ Creates/updates `node_modules/` at the **root level** (not here)
|
|
27
|
-
3. ❌ Does NOT create local `node_modules/` (workspace hoisting)
|
|
28
|
-
4. ❌ Does NOT build or create dist files
|
|
29
|
-
5. ❌ Does NOT modify source files
|
|
22
|
+
### 1. Mount point in template
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
Render a stable DOM node only when you are ready to mount RewardPlay (e.g. after you have a token):
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
```html
|
|
27
|
+
<div v-show="initialized" id="rewardplay-mount-point"></div>
|
|
28
|
+
```
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
- Use `v-show="initialized"` (or `v-if`) so the element exists only when `initialized` is true, then mount in a `nextTick` after setting `initialized = true`.
|
|
31
|
+
- The ID (`rewardplay-mount-point`) is what you pass to `document.getElementById` before mounting.
|
|
32
|
+
|
|
33
|
+
### 2. Initialize and mount in your app
|
|
34
|
+
|
|
35
|
+
Example (Vue 3 Composition API with `createApp`):
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { createApp, ref, nextTick } from 'vue'
|
|
39
|
+
import RewardPlay, { RewardPlayPage } from '@kennofizet/rewardplay-frontend'
|
|
40
|
+
|
|
41
|
+
const initialized = ref(false)
|
|
42
|
+
let rewardPlayApp = null
|
|
43
|
+
|
|
44
|
+
async function initialize() {
|
|
45
|
+
const backendUrl = 'https://your-backend.com' // from your config
|
|
46
|
+
if (!backendUrl?.trim()) {
|
|
47
|
+
// handle error
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const token = await fetchToken() // get token from your backend (see rewardplay-backend)
|
|
53
|
+
initialized.value = true
|
|
54
|
+
await nextTick()
|
|
55
|
+
|
|
56
|
+
const mountPoint = document.getElementById('rewardplay-mount-point')
|
|
57
|
+
if (!mountPoint) {
|
|
58
|
+
initialized.value = false
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
rewardPlayApp = createApp(RewardPlayPage, {
|
|
63
|
+
imageUrls: [],
|
|
64
|
+
scriptUrls: [],
|
|
65
|
+
stylesheetUrls: [],
|
|
66
|
+
fontUrls: [],
|
|
67
|
+
backgroundImage: null,
|
|
68
|
+
language: 'vi', // 'en' | 'vi'
|
|
69
|
+
// enableUnzip: true, // if your RewardPlayPage supports it
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
rewardPlayApp.use(RewardPlay, {
|
|
73
|
+
backendUrl: backendUrl.trim(),
|
|
74
|
+
token,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
rewardPlayApp.mount(mountPoint)
|
|
78
|
+
} catch (err) {
|
|
79
|
+
initialized.value = false
|
|
80
|
+
// handle err
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// e.g. in onMounted or after login
|
|
85
|
+
onMounted(() => {
|
|
86
|
+
initialize()
|
|
87
|
+
})
|
|
88
|
+
```
|
|
36
89
|
|
|
37
|
-
|
|
90
|
+
### 3. Plugin options (required)
|
|
38
91
|
|
|
39
|
-
|
|
92
|
+
| Option | Type | Description |
|
|
93
|
+
|------------|--------|--------------------------------|
|
|
94
|
+
| `backendUrl` | string | Base URL of RewardPlay API |
|
|
95
|
+
| `token` | string | RewardPlay auth token |
|
|
40
96
|
|
|
41
|
-
|
|
97
|
+
Both are required when calling `app.use(RewardPlay, { backendUrl, token })`.
|
|
42
98
|
|
|
43
|
-
|
|
44
|
-
npm run production
|
|
45
|
-
```
|
|
99
|
+
### 4. RewardPlayPage root props (optional)
|
|
46
100
|
|
|
47
|
-
|
|
101
|
+
| Prop | Type | Default | Description |
|
|
102
|
+
|------------------|--------|--------|--------------------------------|
|
|
103
|
+
| `imageUrls` | Array | `[]` | URLs for image manifest/assets |
|
|
104
|
+
| `scriptUrls` | Array | `[]` | Script URLs to load |
|
|
105
|
+
| `stylesheetUrls` | Array | `[]` | CSS URLs |
|
|
106
|
+
| `fontUrls` | Array | `[]` | Font URLs |
|
|
107
|
+
| `backgroundImage`| string | null | Background image URL |
|
|
108
|
+
| `language` | string | `'en'` | UI language: `'en'` or `'vi'` |
|
|
109
|
+
| `rotate` | boolean| true | Auto-rotate in portrait |
|
|
110
|
+
| `customStyles` | Object | `{}` | Extra inline/CSS variables |
|
|
48
111
|
|
|
49
|
-
##
|
|
112
|
+
## Backend and data
|
|
50
113
|
|
|
51
|
-
|
|
114
|
+
- **Token**: Your host app must obtain the RewardPlay token (e.g. from your own backend that talks to RewardPlay). How to issue/validate the token and which endpoints to call is defined in **@kennofizet/rewardplay-backend**.
|
|
115
|
+
- **API**: The frontend calls endpoints under `backendUrl` (e.g. `/api/rewardplay/auth/user-data`, `/api/rewardplay/player/shop`, etc.). Base URL, routes, and response shapes are documented in the backend package.
|
|
116
|
+
- For API contracts, env vars, and server setup, read **@kennofizet/rewardplay-backend** (or the backend README in the same repo).
|
|
52
117
|
|
|
53
|
-
##
|
|
118
|
+
## Exports
|
|
54
119
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
120
|
+
- **default**: plugin object `{ install }` — use with `app.use(RewardPlay, { backendUrl, token })`.
|
|
121
|
+
- **RewardPlayPage**: root Vue component to mount.
|
|
122
|
+
- **installGameModule**, **createGameApi**, **RewardPlayPage**, **ComingSoonPage**, **LoadingSource**, **LoginScreen**.
|
|
123
|
+
- Utilities: **ResourceLoader**, **useResourceLoader**, constants helpers, **createTranslator**, **translations**.
|
|
59
124
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
If you work inside the repo (e.g. `rewardplay-packages`):
|
|
128
|
+
|
|
129
|
+
- Dependencies are usually installed at the **root** of the repo: `npm install` at root.
|
|
130
|
+
- The package entry is `src/index.js`; the host can resolve source directly (no build step required for development).
|
|
131
|
+
- For a production build of the frontend package, run `npm run production` in this package (output in `dist/`).
|
package/package.json
CHANGED
package/src/api/index.js
CHANGED
|
@@ -24,12 +24,150 @@ export function createGameApi(backendUrl, token) {
|
|
|
24
24
|
},
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
// Add interceptor to automatically include zone_id from localStorage as header
|
|
28
|
+
api.interceptors.request.use((config) => {
|
|
29
|
+
try {
|
|
30
|
+
const selectedZone = localStorage.getItem('selected_zone')
|
|
31
|
+
if (selectedZone) {
|
|
32
|
+
const zone = JSON.parse(selectedZone)
|
|
33
|
+
if (zone && zone.id) {
|
|
34
|
+
// Add zone_id to headers (similar to X-RewardPlay-Token)
|
|
35
|
+
config.headers = config.headers || {}
|
|
36
|
+
if (!config.headers['X-RewardPlay-Zone-Id']) {
|
|
37
|
+
config.headers['X-RewardPlay-Zone-Id'] = zone.id.toString()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Silently fail if localStorage is not available or zone is invalid
|
|
43
|
+
console.warn('Failed to get zone_id from localStorage:', e)
|
|
44
|
+
}
|
|
45
|
+
// When sending FormData, remove Content-Type so the browser sets multipart/form-data with boundary
|
|
46
|
+
if (config.data instanceof FormData) {
|
|
47
|
+
delete config.headers['Content-Type']
|
|
48
|
+
}
|
|
49
|
+
return config
|
|
50
|
+
})
|
|
51
|
+
|
|
27
52
|
return {
|
|
28
53
|
// Auth
|
|
29
|
-
checkUser: () => api.
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
54
|
+
checkUser: () => api.get('/api/rewardplay/auth/check'),
|
|
55
|
+
getUserData: () => api.get('/api/rewardplay/auth/user-data'),
|
|
56
|
+
// Zones the current user belongs to
|
|
57
|
+
getZones: () => api.get('/api/rewardplay/player/zones'),
|
|
58
|
+
getCustomImages: (params) => api.get('/api/rewardplay/player/custom-images', { params }),
|
|
59
|
+
// Manifest
|
|
60
|
+
getManifest: () => api.get('/api/rewardplay/manifest'),
|
|
61
|
+
// Ranking (period: day | week | month | year)
|
|
62
|
+
getRanking: (period = 'day') => api.get('/api/rewardplay/ranking', { params: { period } }),
|
|
63
|
+
// Zones the current user can manage (for settings)
|
|
64
|
+
getManagedZones: () => api.get('/api/rewardplay/player/managed-zones'),
|
|
65
|
+
// Zone management (settings)
|
|
66
|
+
getAllZones: (params) => api.get('/api/rewardplay/zones', { params }),
|
|
67
|
+
createZone: (data) => api.post('/api/rewardplay/zones', data),
|
|
68
|
+
updateZone: (id, data) => api.put(`/api/rewardplay/zones/${id}`, data),
|
|
69
|
+
deleteZone: (id) => api.delete(`/api/rewardplay/zones/${id}`),
|
|
70
|
+
// Zone users (server members + assigned)
|
|
71
|
+
getZoneUsers: (zoneId, params) => api.get(`/api/rewardplay/zones/${zoneId}/users`, { params }),
|
|
72
|
+
assignZoneUser: (zoneId, userId) => api.post(`/api/rewardplay/zones/${zoneId}/users`, { user_id: userId }),
|
|
73
|
+
removeZoneUser: (zoneId, userId) => api.delete(`/api/rewardplay/zones/${zoneId}/users/${userId}`),
|
|
74
|
+
|
|
75
|
+
// Setting Items CRUD
|
|
76
|
+
getSettingItems: (params) => api.get('/api/rewardplay/setting-items', { params }),
|
|
77
|
+
suggestSettingItems: () => api.post('/api/rewardplay/setting-items/suggest'),
|
|
78
|
+
createSettingItem: (data) => api.post('/api/rewardplay/setting-items', data),
|
|
79
|
+
updateSettingItem: (id, data) => {
|
|
80
|
+
if (data instanceof FormData) {
|
|
81
|
+
if (!data.has('_method')) data.append('_method', 'PUT')
|
|
82
|
+
return api.post(`/api/rewardplay/setting-items/${id}`, data)
|
|
83
|
+
}
|
|
84
|
+
return api.put(`/api/rewardplay/setting-items/${id}`, data)
|
|
85
|
+
},
|
|
86
|
+
deleteSettingItem: (id) => api.delete(`/api/rewardplay/setting-items/${id}`),
|
|
87
|
+
|
|
88
|
+
// Setting Options CRUD
|
|
89
|
+
getSettingOptions: (params) => api.get('/api/rewardplay/setting-options', { params }),
|
|
90
|
+
getSettingOption: (id) => api.get(`/api/rewardplay/setting-options/${id}`),
|
|
91
|
+
createSettingOption: (data) => api.post('/api/rewardplay/setting-options', data),
|
|
92
|
+
updateSettingOption: (id, data) => api.put(`/api/rewardplay/setting-options/${id}`, data),
|
|
93
|
+
deleteSettingOption: (id) => api.delete(`/api/rewardplay/setting-options/${id}`),
|
|
94
|
+
|
|
95
|
+
// Setting Stats Transforms CRUD
|
|
96
|
+
getSettingStatsTransforms: (params) => api.get('/api/rewardplay/setting-stats-transforms', { params }),
|
|
97
|
+
createSettingStatsTransform: (data) => api.post('/api/rewardplay/setting-stats-transforms', data),
|
|
98
|
+
updateSettingStatsTransform: (id, data) => api.put(`/api/rewardplay/setting-stats-transforms/${id}`, data),
|
|
99
|
+
deleteSettingStatsTransform: (id) => api.delete(`/api/rewardplay/setting-stats-transforms/${id}`),
|
|
100
|
+
suggestSettingStatsTransforms: () => api.post('/api/rewardplay/setting-stats-transforms/suggest'),
|
|
101
|
+
getSettingStatsTransformsAllowedKeys: () => api.get('/api/rewardplay/setting-stats-transforms/allowed-keys'),
|
|
102
|
+
|
|
103
|
+
// Global data (accessible to both player and manage)
|
|
104
|
+
getAllStats: () => api.get('/api/rewardplay/stats/all'),
|
|
105
|
+
getRewardTypes: (mode) => api.get('/api/rewardplay/stats/reward-types', { params: { mode } }),
|
|
106
|
+
|
|
107
|
+
// Setting Item Sets CRUD
|
|
108
|
+
getSettingItemSets: (params) => api.get('/api/rewardplay/setting-item-sets', { params }),
|
|
109
|
+
getItemsForZone: (params) => api.get('/api/rewardplay/setting-items/items-for-zone', { params }),
|
|
110
|
+
createSettingItemSet: (data) => api.post('/api/rewardplay/setting-item-sets', data),
|
|
111
|
+
updateSettingItemSet: (id, data) => api.put(`/api/rewardplay/setting-item-sets/${id}`, data),
|
|
112
|
+
deleteSettingItemSet: (id) => api.delete(`/api/rewardplay/setting-item-sets/${id}`),
|
|
113
|
+
|
|
114
|
+
// Setting Stack Bonuses CRUD
|
|
115
|
+
getStackBonuses: (params) => api.get('/api/rewardplay/setting-stack-bonuses', { params }),
|
|
116
|
+
getStackBonus: (id) => api.get(`/api/rewardplay/setting-stack-bonuses/${id}`),
|
|
117
|
+
createStackBonus: (data) => api.post('/api/rewardplay/setting-stack-bonuses', data),
|
|
118
|
+
updateStackBonus: (id, data) => api.put(`/api/rewardplay/setting-stack-bonuses/${id}`, data),
|
|
119
|
+
deleteStackBonus: (id) => api.delete(`/api/rewardplay/setting-stack-bonuses/${id}`),
|
|
120
|
+
suggestStackBonuses: () => api.post('/api/rewardplay/setting-stack-bonuses/suggest'),
|
|
121
|
+
|
|
122
|
+
// Setting Daily Rewards (Manage)
|
|
123
|
+
getDailyRewardConfigs: (params) => api.get('/api/rewardplay/setting-daily-rewards', { params }), // Expects month/year
|
|
124
|
+
saveDailyRewardConfig: (data) => api.post('/api/rewardplay/setting-daily-rewards', data), // Update or Create based on date
|
|
125
|
+
suggestDailyRewards: (data) => api.post('/api/rewardplay/setting-daily-rewards/suggest', data),
|
|
126
|
+
|
|
127
|
+
// Setting Level Exps (Manage)
|
|
128
|
+
getLevelExps: (params) => api.get('/api/rewardplay/setting-level-exps', { params }),
|
|
129
|
+
getLevelExp: (id) => api.get(`/api/rewardplay/setting-level-exps/${id}`),
|
|
130
|
+
createLevelExp: (data) => api.post('/api/rewardplay/setting-level-exps', data),
|
|
131
|
+
updateLevelExp: (id, data) => api.put(`/api/rewardplay/setting-level-exps/${id}`, data),
|
|
132
|
+
deleteLevelExp: (id) => api.delete(`/api/rewardplay/setting-level-exps/${id}`),
|
|
133
|
+
suggestLevelExps: () => api.post('/api/rewardplay/setting-level-exps/suggest'),
|
|
134
|
+
|
|
135
|
+
// Setting Events (Manage)
|
|
136
|
+
getSettingEvents: (params) => api.get('/api/rewardplay/setting-events', { params }),
|
|
137
|
+
getSettingEvent: (id) => api.get(`/api/rewardplay/setting-events/${id}`),
|
|
138
|
+
suggestSettingEvents: () => api.post('/api/rewardplay/setting-events/suggest'),
|
|
139
|
+
createSettingEvent: (data) => api.post('/api/rewardplay/setting-events', data),
|
|
140
|
+
updateSettingEvent: (id, data) => {
|
|
141
|
+
if (data instanceof FormData) {
|
|
142
|
+
return api.post(`/api/rewardplay/setting-events/${id}`, data)
|
|
143
|
+
}
|
|
144
|
+
return api.put(`/api/rewardplay/setting-events/${id}`, data)
|
|
145
|
+
},
|
|
146
|
+
deleteSettingEvent: (id) => api.delete(`/api/rewardplay/setting-events/${id}`),
|
|
147
|
+
|
|
148
|
+
// Setting Shop Items (Manage)
|
|
149
|
+
getSettingShopItems: (params) => api.get('/api/rewardplay/setting-shop-items', { params }),
|
|
150
|
+
getSettingShopItem: (id) => api.get(`/api/rewardplay/setting-shop-items/${id}`),
|
|
151
|
+
suggestSettingShopItems: () => api.post('/api/rewardplay/setting-shop-items/suggest'),
|
|
152
|
+
createSettingShopItem: (data) => api.post('/api/rewardplay/setting-shop-items', data),
|
|
153
|
+
updateSettingShopItem: (id, data) => api.put(`/api/rewardplay/setting-shop-items/${id}`, data),
|
|
154
|
+
deleteSettingShopItem: (id) => api.delete(`/api/rewardplay/setting-shop-items/${id}`),
|
|
155
|
+
|
|
156
|
+
// Player Daily Rewards & Bag
|
|
157
|
+
getPlayerDailyRewardState: () => api.get('/api/rewardplay/player/daily-rewards'), // Includes stack info
|
|
158
|
+
collectDailyReward: () => api.post('/api/rewardplay/player/daily-rewards/collect'),
|
|
159
|
+
getPlayerBag: () => api.get('/api/rewardplay/player/bag'),
|
|
160
|
+
saveGears: (gears) => api.post('/api/rewardplay/player/bag/gears', { gears }),
|
|
161
|
+
openBox: (userBagItemId, quantity = 1) =>
|
|
162
|
+
api.post('/api/rewardplay/player/bag/open-box', { user_bag_item_id: userBagItemId, quantity: Math.max(1, Math.min(parseInt(quantity, 10) || 1, 99)) }),
|
|
163
|
+
|
|
164
|
+
// Player Events (active events for popup)
|
|
165
|
+
getPlayerEvents: () => api.get('/api/rewardplay/player/events'),
|
|
166
|
+
|
|
167
|
+
// Player Shop (active shop items + purchase)
|
|
168
|
+
getPlayerShop: () => api.get('/api/rewardplay/player/shop'),
|
|
169
|
+
purchaseShopItem: (shopItemId, quantity = 1) =>
|
|
170
|
+
api.post('/api/rewardplay/player/shop/purchase', { shop_item_id: shopItemId, quantity }),
|
|
33
171
|
}
|
|
34
172
|
}
|
|
35
173
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="bonus-section">
|
|
3
|
+
<label>{{ label }}</label>
|
|
4
|
+
<div class="bonus-list">
|
|
5
|
+
<div
|
|
6
|
+
v-for="(bonus, index) in bonuses"
|
|
7
|
+
:key="index"
|
|
8
|
+
class="bonus-item"
|
|
9
|
+
>
|
|
10
|
+
<template v-if="bonus.type === 'custom_option' && bonus.name">
|
|
11
|
+
<!-- Show display component for custom options (snapshot) -->
|
|
12
|
+
<CustomOptionDisplay
|
|
13
|
+
:name="bonus.name"
|
|
14
|
+
:properties="bonus.properties || {}"
|
|
15
|
+
:stats-label="t('page.manageSetting.settingItems.stats')"
|
|
16
|
+
class="bonus-custom-display"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
19
|
+
<template v-else>
|
|
20
|
+
<!-- Show unified selector for stats or when nothing selected yet -->
|
|
21
|
+
<CustomSelect
|
|
22
|
+
v-model="bonus.selectedValue"
|
|
23
|
+
:options="unifiedOptions"
|
|
24
|
+
:placeholder="selectKeyPlaceholder"
|
|
25
|
+
@change="handleKeyChange(index)"
|
|
26
|
+
trigger-class="bonus-key-select"
|
|
27
|
+
/>
|
|
28
|
+
<input
|
|
29
|
+
v-if="bonus.type === 'stat' && bonus.selectedValue"
|
|
30
|
+
v-model.number="bonus.value"
|
|
31
|
+
type="number"
|
|
32
|
+
step="0.01"
|
|
33
|
+
:placeholder="valuePlaceholder"
|
|
34
|
+
class="bonus-value-input"
|
|
35
|
+
/>
|
|
36
|
+
</template>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
class="btn-remove-bonus"
|
|
40
|
+
@click="handleRemove(index)"
|
|
41
|
+
:title="removeLabel"
|
|
42
|
+
>
|
|
43
|
+
×
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
class="btn-add-bonus"
|
|
49
|
+
@click="handleAdd"
|
|
50
|
+
>
|
|
51
|
+
<span class="btn-add-icon">+</span>
|
|
52
|
+
<span>{{ addOptionLabel }}</span>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup>
|
|
59
|
+
import { inject, computed } from 'vue'
|
|
60
|
+
import CustomSelect from './CustomSelect.vue'
|
|
61
|
+
import CustomOptionDisplay from './CustomOptionDisplay.vue'
|
|
62
|
+
|
|
63
|
+
const translator = inject('translator', null)
|
|
64
|
+
const t = translator || ((key) => key)
|
|
65
|
+
|
|
66
|
+
const props = defineProps({
|
|
67
|
+
label: {
|
|
68
|
+
type: String,
|
|
69
|
+
required: true
|
|
70
|
+
},
|
|
71
|
+
bonuses: {
|
|
72
|
+
type: Array,
|
|
73
|
+
required: true
|
|
74
|
+
},
|
|
75
|
+
bonusKeyOptions: {
|
|
76
|
+
type: Array,
|
|
77
|
+
required: true
|
|
78
|
+
},
|
|
79
|
+
customOptionOptions: {
|
|
80
|
+
type: Array,
|
|
81
|
+
default: () => []
|
|
82
|
+
},
|
|
83
|
+
selectKeyPlaceholder: {
|
|
84
|
+
type: String,
|
|
85
|
+
default: 'Select Key'
|
|
86
|
+
},
|
|
87
|
+
valuePlaceholder: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: 'Value'
|
|
90
|
+
},
|
|
91
|
+
addOptionLabel: {
|
|
92
|
+
type: String,
|
|
93
|
+
default: 'Add Option'
|
|
94
|
+
},
|
|
95
|
+
removeLabel: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: 'Remove'
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const emit = defineEmits(['add', 'remove', 'key-change'])
|
|
102
|
+
|
|
103
|
+
// Unified options combining stats and custom options
|
|
104
|
+
const unifiedOptions = computed(() => {
|
|
105
|
+
const options = []
|
|
106
|
+
|
|
107
|
+
// Add regular stats
|
|
108
|
+
props.bonusKeyOptions.forEach(stat => {
|
|
109
|
+
options.push({
|
|
110
|
+
value: stat.value,
|
|
111
|
+
label: stat.label,
|
|
112
|
+
type: 'stat',
|
|
113
|
+
isCustom: false
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Add custom options
|
|
118
|
+
props.customOptionOptions.forEach(customOption => {
|
|
119
|
+
options.push({
|
|
120
|
+
value: customOption.value,
|
|
121
|
+
label: customOption.label,
|
|
122
|
+
type: 'custom_option',
|
|
123
|
+
isCustom: true,
|
|
124
|
+
properties: customOption.properties
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return options
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const handleAdd = () => {
|
|
132
|
+
emit('add')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleRemove = (index) => {
|
|
136
|
+
emit('remove', index)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const handleKeyChange = (index) => {
|
|
140
|
+
emit('key-change', index)
|
|
141
|
+
}
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
.bonus-section {
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.bonus-section label {
|
|
151
|
+
margin-bottom: 5px;
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
color: #d0d4d6;
|
|
154
|
+
font-weight: 500;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.bonus-list {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
gap: 10px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.bonus-item {
|
|
164
|
+
display: flex;
|
|
165
|
+
gap: 8px;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.bonus-key-select {
|
|
170
|
+
flex: 1;
|
|
171
|
+
min-width: 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.bonus-value-input {
|
|
175
|
+
flex: 1;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
padding: 8px 10px;
|
|
178
|
+
background: #253344;
|
|
179
|
+
border: 1px solid #1a2332;
|
|
180
|
+
color: #d0d4d6;
|
|
181
|
+
font-size: 14px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.bonus-custom-value {
|
|
185
|
+
flex: 1;
|
|
186
|
+
display: flex;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
gap: 4px;
|
|
189
|
+
padding: 8px 10px;
|
|
190
|
+
background: #1a2332;
|
|
191
|
+
border: 1px solid #253344;
|
|
192
|
+
border-radius: 4px;
|
|
193
|
+
cursor: help;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.custom-value-label {
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
color: #f6a901;
|
|
199
|
+
font-weight: 500;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.custom-value-count {
|
|
203
|
+
font-size: 11px;
|
|
204
|
+
color: #999;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.btn-remove-bonus {
|
|
208
|
+
padding: 8px 12px;
|
|
209
|
+
background: #ff6b6b;
|
|
210
|
+
color: white;
|
|
211
|
+
border: none;
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
font-size: 18px;
|
|
215
|
+
line-height: 1;
|
|
216
|
+
transition: background 0.2s;
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.btn-remove-bonus:hover {
|
|
221
|
+
background: #ee5a5a;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.btn-add-bonus {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
gap: 6px;
|
|
229
|
+
padding: 10px 16px;
|
|
230
|
+
background: #253344;
|
|
231
|
+
border: 2px dashed #1a2332;
|
|
232
|
+
color: #d0d4d6;
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
font-size: 14px;
|
|
235
|
+
border-radius: 4px;
|
|
236
|
+
transition: all 0.2s;
|
|
237
|
+
width: 100%;
|
|
238
|
+
margin-top: 5px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.btn-add-bonus:hover {
|
|
242
|
+
background: #1a2332;
|
|
243
|
+
border-color: #f6a901;
|
|
244
|
+
color: #f6a901;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.btn-add-icon {
|
|
248
|
+
font-size: 18px;
|
|
249
|
+
font-weight: bold;
|
|
250
|
+
line-height: 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.bonus-custom-display {
|
|
254
|
+
flex: 1;
|
|
255
|
+
}
|
|
256
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="custom-option-display"
|
|
4
|
+
:title="tooltipText"
|
|
5
|
+
>
|
|
6
|
+
<span class="custom-option-name">{{ name }}</span>
|
|
7
|
+
<span class="custom-option-info">{{ statsCount }} {{ statsLabel }}</span>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import { computed, inject } from 'vue'
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
name: {
|
|
16
|
+
type: String,
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
properties: {
|
|
20
|
+
type: Object,
|
|
21
|
+
default: () => ({})
|
|
22
|
+
},
|
|
23
|
+
statsLabel: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'stats'
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const statsCount = computed(() => {
|
|
30
|
+
return Object.keys(props.properties || {}).length
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const tooltipText = computed(() => {
|
|
34
|
+
if (!props.properties || Object.keys(props.properties).length === 0) {
|
|
35
|
+
return props.name
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Format properties for tooltip display
|
|
39
|
+
const statsList = Object.entries(props.properties)
|
|
40
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
41
|
+
.join('\n')
|
|
42
|
+
|
|
43
|
+
return `${props.name}\n\n${statsList}`
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<style scoped>
|
|
48
|
+
.custom-option-display {
|
|
49
|
+
flex: 1;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
padding: 8px 10px;
|
|
54
|
+
background: #1a2332;
|
|
55
|
+
border: 1px solid #253344;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
cursor: help;
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.custom-option-display.property-display {
|
|
62
|
+
min-width: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.custom-option-name {
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
color: #f6a901;
|
|
68
|
+
font-size: 14px;
|
|
69
|
+
margin-right: 8px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.custom-option-info {
|
|
73
|
+
font-size: 11px;
|
|
74
|
+
color: #8a9196;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
}
|
|
77
|
+
</style>
|