3 changed files with 604 additions and 0 deletions
@ -0,0 +1,548 @@ |
|||
# 背包功能 MVP 实现计划 |
|||
|
|||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
|||
|
|||
**Goal:** 在游戏客户端添加背包展示页面,显示角色背包中的物品,鼠标悬停显示物品名称和基本信息 |
|||
|
|||
**Architecture:** 使用现有后端API,前端本地分页,每页16个物品(4×4网格) |
|||
|
|||
**Tech Stack:** Vue 3 + TypeScript + Pinia + Element Plus |
|||
|
|||
--- |
|||
|
|||
### Task 1: 添加背包 API 模块 |
|||
|
|||
**Files:** |
|||
- Create: `Build_God_Game/src/api/bag.ts` |
|||
|
|||
- [ ] **Step 1: 创建 bag.ts API 模块** |
|||
|
|||
```typescript |
|||
import http from './index' |
|||
|
|||
export interface Bag { |
|||
id: number |
|||
name: string |
|||
rarity: number |
|||
capacity: number |
|||
description: string | null |
|||
} |
|||
|
|||
export interface CharacterBag { |
|||
id: number |
|||
characterId: number |
|||
bagId: number |
|||
bagName: string | null |
|||
bagCapacity: number |
|||
} |
|||
|
|||
export interface BagItem { |
|||
id: number |
|||
characterBagId: number |
|||
itemType: number |
|||
itemId: number |
|||
quantity: number |
|||
itemName: string | null |
|||
itemRarity: number | null |
|||
} |
|||
|
|||
export const getCharacterBag = (characterId: number): Promise<CharacterBag | null> => { |
|||
return http.get(`bag/character/${characterId}`) |
|||
} |
|||
|
|||
export const getBagItems = (characterBagId: number): Promise<BagItem[]> => { |
|||
return http.get(`bag/${characterBagId}/items`) |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 2: 提交代码** |
|||
|
|||
```bash |
|||
git add Build_God_Game/src/api/bag.ts |
|||
git commit -m "feat(game): add bag API module" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 2: 添加背包 Store |
|||
|
|||
**Files:** |
|||
- Create: `Build_God_Game/src/stores/bag.ts` |
|||
|
|||
- [ ] **Step 1: 创建 bag.ts Store** |
|||
|
|||
```typescript |
|||
import { defineStore } from 'pinia' |
|||
import { ref, computed } from 'vue' |
|||
import { getCharacterBag, getBagItems, type CharacterBag, type BagItem } from '@/api/bag' |
|||
import { useCharacterStore } from './character' |
|||
|
|||
const PAGE_SIZE = 16 |
|||
|
|||
export const useBagStore = defineStore('bag', () => { |
|||
const characterStore = useCharacterStore() |
|||
|
|||
const characterBag = ref<CharacterBag | null>(null) |
|||
const bagItems = ref<BagItem[]>([]) |
|||
const currentPage = ref(1) |
|||
const loading = ref(false) |
|||
const error = ref<string | null>(null) |
|||
|
|||
const currentCharacterId = computed(() => characterStore.currentCharacter?.id) |
|||
|
|||
const totalItems = computed(() => bagItems.value.length) |
|||
|
|||
const totalPages = computed(() => Math.ceil(totalItems.value / PAGE_SIZE)) |
|||
|
|||
const paginatedItems = computed(() => { |
|||
const start = (currentPage.value - 1) * PAGE_SIZE |
|||
const end = start + PAGE_SIZE |
|||
return bagItems.value.slice(start, end) |
|||
}) |
|||
|
|||
const usedCapacity = computed(() => { |
|||
return bagItems.value.reduce((sum, item) => sum + item.quantity, 0) |
|||
}) |
|||
|
|||
const loadBag = async () => { |
|||
if (!currentCharacterId.value) { |
|||
error.value = '请先选择角色' |
|||
return |
|||
} |
|||
|
|||
try { |
|||
loading.value = true |
|||
error.value = null |
|||
|
|||
characterBag.value = await getCharacterBag(currentCharacterId.value) |
|||
|
|||
if (characterBag.value) { |
|||
bagItems.value = await getBagItems(characterBag.value.id) |
|||
} else { |
|||
bagItems.value = [] |
|||
} |
|||
|
|||
currentPage.value = 1 |
|||
} catch (e: any) { |
|||
error.value = e.message || '加载背包失败' |
|||
console.error('Load bag error:', e) |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
const nextPage = () => { |
|||
if (currentPage.value < totalPages.value) { |
|||
currentPage.value++ |
|||
} |
|||
} |
|||
|
|||
const prevPage = () => { |
|||
if (currentPage.value > 1) { |
|||
currentPage.value-- |
|||
} |
|||
} |
|||
|
|||
return { |
|||
characterBag, |
|||
bagItems, |
|||
currentPage, |
|||
totalPages, |
|||
totalItems, |
|||
paginatedItems, |
|||
usedCapacity, |
|||
loading, |
|||
error, |
|||
loadBag, |
|||
nextPage, |
|||
prevPage |
|||
} |
|||
}) |
|||
``` |
|||
|
|||
- [ ] **Step 2: 提交代码** |
|||
|
|||
```bash |
|||
git add Build_God_Game/src/stores/bag.ts |
|||
git commit -m "feat(game): add bag store" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 3: 添加背包页面 |
|||
|
|||
**Files:** |
|||
- Create: `Build_God_Game/src/views/BagView.vue` |
|||
|
|||
- [ ] **Step 1: 创建 BagView.vue** |
|||
|
|||
```vue |
|||
<script setup lang="ts"> |
|||
import { onMounted, computed } from 'vue' |
|||
import { useRouter } from 'vue-router' |
|||
import { useBagStore } from '@/stores/bag' |
|||
import { useCharacterStore } from '@/stores/character' |
|||
import StarBorder from '@/components/StarBorder/StarBorder.vue' |
|||
import Particles from '@/components/Particles/Particles.vue' |
|||
import { ArrowLeft } from '@element-plus/icons-vue' |
|||
|
|||
const router = useRouter() |
|||
const bagStore = useBagStore() |
|||
const characterStore = useCharacterStore() |
|||
|
|||
const characterName = computed(() => characterStore.currentCharacter?.name || '') |
|||
const bagName = computed(() => bagStore.characterBag?.bagName || '背包') |
|||
const bagCapacity = computed(() => bagStore.characterBag?.bagCapacity || 0) |
|||
const usedCapacity = computed(() => bagStore.usedCapacity) |
|||
|
|||
const itemTypeMap: Record<number, string> = { |
|||
1: '装备', |
|||
2: '丹药' |
|||
} |
|||
|
|||
const rarityMap: Record<number, string> = { |
|||
1: '普通', |
|||
2: '稀有', |
|||
3: '史诗', |
|||
4: '传说' |
|||
} |
|||
|
|||
const getItemTooltip = (item: typeof bagStore.paginatedItems[0]) => { |
|||
const type = itemTypeMap[item.itemType] || '未知' |
|||
const rarity = item.itemRarity ? rarityMap[item.itemRarity] || '' : '' |
|||
return `${item.itemName}\n类型: ${type}${rarity ? ` | 稀有度: ${rarity}` : ''}\n数量: ${item.quantity}` |
|||
} |
|||
|
|||
const handleBack = () => { |
|||
router.push('/game') |
|||
} |
|||
|
|||
onMounted(() => { |
|||
bagStore.loadBag() |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="bag-page"> |
|||
<Particles :particle-count="30" :particle-colors="['#ffffff', '#cccccc']" class="particles-bg" /> |
|||
|
|||
<div class="bag-container"> |
|||
<StarBorder as="div" color="#8b5cf6" speed="5s" :thickness="2" class="back-btn" @click="handleBack"> |
|||
<div class="back-content"> |
|||
<el-icon><ArrowLeft /></el-icon> |
|||
<span>返回</span> |
|||
</div> |
|||
</StarBorder> |
|||
|
|||
<div class="bag-header"> |
|||
<h2>{{ bagName }}</h2> |
|||
<span class="capacity">{{ usedCapacity }} / {{ bagCapacity }}</span> |
|||
</div> |
|||
|
|||
<div v-if="bagStore.loading" class="loading"> |
|||
加载中... |
|||
</div> |
|||
|
|||
<div v-else-if="bagStore.error" class="error"> |
|||
{{ bagStore.error }} |
|||
</div> |
|||
|
|||
<div v-else-if="bagStore.totalItems === 0" class="empty"> |
|||
背包空空如也,快去获取一些物品吧! |
|||
</div> |
|||
|
|||
<div v-else class="items-grid"> |
|||
<el-tooltip |
|||
v-for="item in bagStore.paginatedItems" |
|||
:key="item.id" |
|||
:content="getItemTooltip(item)" |
|||
placement="top" |
|||
:show-after="300" |
|||
> |
|||
<div class="item-cell"> |
|||
<div class="item-icon" :class="'rarity-' + (item.itemRarity || 1)"> |
|||
{{ item.itemType === 1 ? '⚔️' : '💊' }} |
|||
</div> |
|||
<span class="item-name">{{ item.itemName }}</span> |
|||
<span v-if="item.quantity > 1" class="item-count">{{ item.quantity }}</span> |
|||
</div> |
|||
</el-tooltip> |
|||
</div> |
|||
|
|||
<div v-if="bagStore.totalPages > 1" class="pagination"> |
|||
<button |
|||
class="page-btn" |
|||
:disabled="bagStore.currentPage === 1" |
|||
@click="bagStore.prevPage()" |
|||
> |
|||
上一页 |
|||
</button> |
|||
<span class="page-info">{{ bagStore.currentPage }} / {{ bagStore.totalPages }}</span> |
|||
<button |
|||
class="page-btn" |
|||
:disabled="bagStore.currentPage >= bagStore.totalPages" |
|||
@click="bagStore.nextPage()" |
|||
> |
|||
下一页 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.bag-page { |
|||
min-height: 100vh; |
|||
background: #000000; |
|||
padding: 20px; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.particles-bg { |
|||
position: fixed !important; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100% !important; |
|||
height: 100% !important; |
|||
} |
|||
|
|||
.bag-container { |
|||
max-width: 480px; |
|||
margin: 0 auto; |
|||
position: relative; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.back-btn { |
|||
margin-bottom: 20px; |
|||
display: inline-block; |
|||
} |
|||
|
|||
.back-content { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
padding: 8px 16px; |
|||
cursor: pointer; |
|||
color: #cccccc; |
|||
background: rgba(255, 255, 255, 0.03); |
|||
border: 1px solid rgba(255, 255, 255, 0.08); |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
.bag-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 24px; |
|||
padding: 16px 20px; |
|||
background: rgba(255, 255, 255, 0.03); |
|||
border: 1px solid rgba(255, 255, 255, 0.08); |
|||
border-radius: 12px; |
|||
} |
|||
|
|||
.bag-header h2 { |
|||
margin: 0; |
|||
color: #ffffff; |
|||
font-size: 1.25rem; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.capacity { |
|||
color: #888888; |
|||
font-size: 0.9rem; |
|||
} |
|||
|
|||
.loading, .error, .empty { |
|||
text-align: center; |
|||
padding: 40px; |
|||
color: #888888; |
|||
} |
|||
|
|||
.error { |
|||
color: #ff4444; |
|||
} |
|||
|
|||
.items-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(4, 1fr); |
|||
gap: 12px; |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.item-cell { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 16px 8px; |
|||
background: rgba(255, 255, 255, 0.03); |
|||
border: 1px solid rgba(255, 255, 255, 0.08); |
|||
border-radius: 12px; |
|||
cursor: pointer; |
|||
transition: all 0.2s ease; |
|||
} |
|||
|
|||
.item-cell:hover { |
|||
background: rgba(255, 255, 255, 0.06); |
|||
border-color: rgba(255, 255, 255, 0.15); |
|||
} |
|||
|
|||
.item-icon { |
|||
font-size: 2rem; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.item-name { |
|||
color: #cccccc; |
|||
font-size: 0.75rem; |
|||
text-align: center; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
width: 100%; |
|||
} |
|||
|
|||
.item-count { |
|||
position: absolute; |
|||
top: 4px; |
|||
right: 4px; |
|||
background: rgba(139, 92, 246, 0.8); |
|||
color: #ffffff; |
|||
font-size: 0.65rem; |
|||
padding: 2px 6px; |
|||
border-radius: 8px; |
|||
min-width: 18px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.rarity-1 { filter: grayscale(0.3); } |
|||
.rarity-2 { filter: hue-rotate(80deg) saturate(1.5); } |
|||
.rarity-3 { filter: hue-rotate(200deg) saturate(2); } |
|||
.rarity-4 { filter: hue-rotate(-30deg) saturate(2) brightness(1.2); } |
|||
|
|||
.pagination { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
gap: 16px; |
|||
} |
|||
|
|||
.page-btn { |
|||
padding: 10px 20px; |
|||
background: rgba(255, 255, 255, 0.05); |
|||
border: 1px solid rgba(255, 255, 255, 0.1); |
|||
border-radius: 8px; |
|||
color: #cccccc; |
|||
cursor: pointer; |
|||
transition: all 0.2s ease; |
|||
} |
|||
|
|||
.page-btn:hover:not(:disabled) { |
|||
background: rgba(255, 255, 255, 0.1); |
|||
} |
|||
|
|||
.page-btn:disabled { |
|||
opacity: 0.4; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
.page-info { |
|||
color: #888888; |
|||
font-size: 0.9rem; |
|||
} |
|||
</style> |
|||
``` |
|||
|
|||
- [ ] **Step 2: 提交代码** |
|||
|
|||
```bash |
|||
git add Build_God_Game/src/views/BagView.vue |
|||
git commit -m "feat(game): add bag view page" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 4: 添加路由 |
|||
|
|||
**Files:** |
|||
- Modify: `Build_God_Game/src/router/index.ts:45-53` |
|||
|
|||
- [ ] **Step 1: 添加背包路由** |
|||
|
|||
在 `routes` 数组中,在 `/scrap` 路由后添加: |
|||
|
|||
```typescript |
|||
{ |
|||
path: '/bag', |
|||
name: 'bag', |
|||
component: () => import('@/views/BagView.vue'), |
|||
meta: { requiresAuth: true } |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 2: 提交代码** |
|||
|
|||
```bash |
|||
git add Build_God_Game/src/router/index.ts |
|||
git commit -m "feat(game): add bag route" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 5: 更新游戏主页导航菜单 |
|||
|
|||
**Files:** |
|||
- Modify: `Build_God_Game/src/views/GameView.vue:41-46` |
|||
|
|||
- [ ] **Step 1: 在 menuItems 中添加背包入口** |
|||
|
|||
找到 `const menuItems = computed(() => [...])`,在数组中添加: |
|||
|
|||
```typescript |
|||
{ label: '背包', icon: '🎒', useImage: false } |
|||
``` |
|||
|
|||
- [ ] **Step 2: 添加 navigateTo 跳转逻辑** |
|||
|
|||
在 `navigateTo` 函数中添加: |
|||
|
|||
```typescript |
|||
} else if (item.label === '背包') { |
|||
router.push('/bag') |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 3: 提交代码** |
|||
|
|||
```bash |
|||
git add Build_God_Game/src/views/GameView.vue |
|||
git commit -m "feat(game): add bag menu entry in game view" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### Task 6: 验证构建 |
|||
|
|||
**Files:** |
|||
- Test: `Build_God_Game/` 整个项目 |
|||
|
|||
- [ ] **Step 1: 运行类型检查** |
|||
|
|||
```bash |
|||
cd Build_God_Game && npm run type-check |
|||
``` |
|||
|
|||
- [ ] **Step 2: 运行构建** |
|||
|
|||
```bash |
|||
cd Build_God_Game && npm run build |
|||
``` |
|||
|
|||
- [ ] **Step 3: 提交最终代码** |
|||
|
|||
```bash |
|||
git add -A && git commit -m "feat(game): complete bag feature MVP" |
|||
``` |
|||
@ -0,0 +1,52 @@ |
|||
# 背包功能 MVP 设计 |
|||
|
|||
## 需求 |
|||
|
|||
在游戏客户端添加背包展示页面,显示角色背包中的物品,鼠标悬停显示物品名称和基本信息。 |
|||
|
|||
## 现有后端 API |
|||
|
|||
- `GET /api/god/bag/character/{characterId}` - 获取角色背包信息(包含背包名称和容量) |
|||
- `GET /api/god/bag/{characterBagId}/items` - 获取背包物品列表 |
|||
- 物品类型:装备(1)、丹药(2) |
|||
|
|||
## 实现方案 |
|||
|
|||
### 1. 添加游戏客户端 API (`src/api/bag.ts`) |
|||
|
|||
- `getCharacterBag(characterId)` - 获取角色背包信息 |
|||
- `getBagItems(characterBagId)` - 获取背包物品 |
|||
|
|||
### 2. 添加 Pinia Store (`src/stores/bag.ts`) |
|||
|
|||
- 管理背包数据、当前页码等状态 |
|||
- 提供加载、刷新方法 |
|||
|
|||
### 3. 添加背包页面 (`src/views/BagView.vue`) |
|||
|
|||
- 顶部:显示背包名称和容量信息(如 "普通背包 5/20") |
|||
- 物品网格:每页显示 **16** 个物品(4列 × 4行) |
|||
- 底部:分页控件(上一页/下一页) |
|||
- 物品格子:显示物品图标、名称、数量角标(仅数量>1时显示) |
|||
- 鼠标悬停:使用 Element Plus 的 `el-tooltip` 显示详细信息 |
|||
|
|||
### 4. 添加路由 (`src/router/index.ts`) |
|||
|
|||
- 路径:`/bag` |
|||
- 需登录访问 |
|||
|
|||
### 5. 更新导航菜单 (`GameView.vue`) |
|||
|
|||
- 在 menuItems 中添加:{ label: '背包', icon: '🎒' } |
|||
|
|||
## 物品信息展示(hover 提示内容) |
|||
|
|||
- **装备**:名称、稀有度(普通/稀有/史诗/传说) |
|||
- **丹药**:名称、功效描述 |
|||
|
|||
## 技术细节 |
|||
|
|||
- 使用前端本地分页(不修改后端) |
|||
- 当前角色从 `characterStore` 获取 |
|||
- 页面访问需要登录验证 |
|||
- 背包容量:普通20格、稀有40格、史诗60格、传说100格 |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"$schema": "https://opencode.ai/config.json", |
|||
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"] |
|||
} |
|||
Loading…
Reference in new issue