You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
548 lines
12 KiB
548 lines
12 KiB
|
1 month ago
|
# 背包功能 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"
|
||
|
|
```
|