Browse Source

feat: add scrap to bag with three-section display

master
qinhan 1 month ago
parent
commit
27fe120aad
  1. 1
      Build_God_Admin_Frontend/Frontend/src/api/scrap.ts
  2. 13
      Build_God_Admin_Frontend/Frontend/src/views/admin/ScrapView.vue
  3. 5
      Build_God_Api/Build_God_Api/DB/BagItem.cs
  4. 6
      Build_God_Api/Build_God_Api/DB/Scrap.cs
  5. 18
      Build_God_Api/Build_God_Api/Services/BagService.cs
  6. 35
      Build_God_Api/Build_God_Api/Services/ScrapService.cs
  7. 6
      Build_God_Game/src/api/bag.ts
  8. 113
      Build_God_Game/src/stores/bag.ts
  9. 244
      Build_God_Game/src/views/BagView.vue

1
Build_God_Admin_Frontend/Frontend/src/api/scrap.ts

@ -13,6 +13,7 @@ export interface Scrap {
hpBonus: number;
magicBonus: number;
isActive: boolean;
icon?: string | null;
}
export interface ScrapLevel {

13
Build_God_Admin_Frontend/Frontend/src/views/admin/ScrapView.vue

@ -35,7 +35,8 @@ const formData = ref<Partial<Scrap>>({
defenseBonus: 0,
hpBonus: 0,
magicBonus: 0,
isActive: true
isActive: true,
icon: null
})
const filteredScraps = computed(() => {
@ -78,7 +79,8 @@ const openDialog = (scrap?: Scrap) => {
defenseBonus: 0,
hpBonus: 0,
magicBonus: 0,
isActive: true
isActive: true,
icon: null
}
}
showDialog.value = true
@ -116,7 +118,8 @@ const saveScrap = async () => {
defenseBonus: formData.value.defenseBonus || 0,
hpBonus: formData.value.hpBonus || 0,
magicBonus: formData.value.magicBonus || 0,
isActive: formData.value.isActive ?? true
isActive: formData.value.isActive ?? true,
icon: formData.value.icon || null
}
const result = await AddScrap(newScrap)
@ -253,6 +256,10 @@ const refreshScraps = async () => {
<el-switch v-model="formData.isActive" />
</el-form-item>
<el-form-item label="图标文件名">
<el-input v-model="formData.icon" placeholder="如: scrap_ancient.png" clearable />
</el-form-item>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveScrap">保存</el-button>

5
Build_God_Api/Build_God_Api/DB/BagItem.cs

@ -38,6 +38,9 @@ namespace Build_God_Api.DB
Equipment = 1,
[Description("丹药")]
Pill = 2
Pill = 2,
[Description("垃圾")]
Scrap = 3
}
}

6
Build_God_Api/Build_God_Api/DB/Scrap.cs

@ -25,6 +25,12 @@ namespace Build_God_Api.DB
public int MagicBonus { get; set; }
public bool IsActive { get; set; } = true;
/// <summary>
/// 图标文件名
/// </summary>
[SugarColumn(IsNullable = true)]
public string? Icon { get; set; }
}
public enum ScrapLevel

18
Build_God_Api/Build_God_Api/Services/BagService.cs

@ -36,6 +36,12 @@ namespace Build_God_Api.Services
public string? ItemName { get; set; }
public int? ItemRarity { get; set; }
public string? Icon { get; set; }
// 垃圾专用属性
public int? AttackBonus { get; set; }
public int? DefenseBonus { get; set; }
public int? HpBonus { get; set; }
public int? MagicBonus { get; set; }
public int? ScrapLevel { get; set; }
}
public class CharacterBagDto
@ -197,6 +203,18 @@ namespace Build_God_Api.Services
dto.ItemName = pill?.Name;
dto.Icon = pill?.Icon;
}
else if (item.ItemType == BagItemType.Scrap)
{
var scrap = await db.Queryable<Scrap>()
.FirstAsync(x => x.Id == item.ItemId);
dto.ItemName = scrap?.Name;
dto.Icon = scrap?.Icon;
dto.AttackBonus = scrap?.AttackBonus;
dto.DefenseBonus = scrap?.DefenseBonus;
dto.HpBonus = scrap?.HpBonus;
dto.MagicBonus = scrap?.MagicBonus;
dto.ScrapLevel = scrap != null ? (int)scrap.Level : null;
}
result.Add(dto);
}

35
Build_God_Api/Build_God_Api/Services/ScrapService.cs

@ -73,14 +73,35 @@ namespace Build_God_Api.Services
var random = new Random();
var selectedScrap = levelScraps[random.Next(levelScraps.Count)];
var characterScrap = new CharacterScrap
// 获取角色背包
var characterBag = await db.Queryable<CharacterBag>()
.FirstAsync(x => x.CharacterId == characterId)
?? throw new Exception("角色没有背包,请先分配背包");
// 检查是否已存在相同垃圾
var existingItem = await db.Queryable<BagItem>()
.FirstAsync(x => x.CharacterBagId == characterBag.Id
&& x.ItemType == BagItemType.Scrap
&& x.ItemId == selectedScrap.Id);
if (existingItem != null)
{
CharacterId = characterId,
ScrapId = selectedScrap.Id,
CreatedBy = characterId,
UpdatedBy = characterId
};
await db.Insertable(characterScrap).ExecuteCommandAsync();
// 已存在则增加数量
existingItem.Quantity += 1;
await db.Updateable(existingItem).ExecuteCommandAsync();
}
else
{
// 新增到背包
var bagItem = new BagItem
{
CharacterBagId = characterBag.Id,
ItemType = BagItemType.Scrap,
ItemId = selectedScrap.Id,
Quantity = 1
};
await db.Insertable(bagItem).ExecuteCommandAsync();
}
return new ScrapScanResultDto
{

6
Build_God_Game/src/api/bag.ts

@ -25,6 +25,12 @@ export interface BagItem {
itemName: string | null
itemRarity: number | null
icon: string | null
// 垃圾专用
attackBonus?: number | null
defenseBonus?: number | null
hpBonus?: number | null
magicBonus?: number | null
scrapLevel?: number | null
}
export const getCharacterBag = (characterId: number): Promise<CharacterBag | null> => {

113
Build_God_Game/src/stores/bag.ts

@ -5,25 +5,54 @@ import { useCharacterStore } from './character'
const PAGE_SIZE = 16
const ITEM_TYPE = {
EQUIPMENT: 1,
PILL: 2,
SCRAP: 3
}
export const useBagStore = defineStore('bag', () => {
const characterStore = useCharacterStore()
const characterBag = ref<CharacterBag | null>(null)
const bagItems = ref<BagItem[]>([])
const currentPage = ref(1)
const equipmentPage = ref(1)
const pillPage = ref(1)
const scrapPage = 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 equipmentItems = computed(() =>
bagItems.value.filter(item => item.itemType === ITEM_TYPE.EQUIPMENT)
)
const pillItems = computed(() =>
bagItems.value.filter(item => item.itemType === ITEM_TYPE.PILL)
)
const scrapItems = computed(() =>
bagItems.value.filter(item => item.itemType === ITEM_TYPE.SCRAP)
)
const totalEquipmentPages = computed(() => Math.ceil(equipmentItems.value.length / PAGE_SIZE))
const totalPillPages = computed(() => Math.ceil(pillItems.value.length / PAGE_SIZE))
const totalScrapPages = computed(() => Math.ceil(scrapItems.value.length / PAGE_SIZE))
const paginatedEquipment = computed(() => {
const start = (equipmentPage.value - 1) * PAGE_SIZE
return equipmentItems.value.slice(start, start + PAGE_SIZE)
})
const paginatedPills = computed(() => {
const start = (pillPage.value - 1) * PAGE_SIZE
return pillItems.value.slice(start, start + PAGE_SIZE)
})
const paginatedScraps = computed(() => {
const start = (scrapPage.value - 1) * PAGE_SIZE
return scrapItems.value.slice(start, start + PAGE_SIZE)
})
const usedCapacity = computed(() => {
@ -48,7 +77,9 @@ export const useBagStore = defineStore('bag', () => {
bagItems.value = []
}
currentPage.value = 1
equipmentPage.value = 1
pillPage.value = 1
scrapPage.value = 1
} catch (e: any) {
error.value = e.message || '加载背包失败'
console.error('Load bag error:', e)
@ -57,30 +88,66 @@ export const useBagStore = defineStore('bag', () => {
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
const nextEquipmentPage = () => {
if (equipmentPage.value < totalEquipmentPages.value) {
equipmentPage.value++
}
}
const prevEquipmentPage = () => {
if (equipmentPage.value > 1) {
equipmentPage.value--
}
}
const nextPillPage = () => {
if (pillPage.value < totalPillPages.value) {
pillPage.value++
}
}
const prevPillPage = () => {
if (pillPage.value > 1) {
pillPage.value--
}
}
const nextScrapPage = () => {
if (scrapPage.value < totalScrapPages.value) {
scrapPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
const prevScrapPage = () => {
if (scrapPage.value > 1) {
scrapPage.value--
}
}
return {
characterBag,
bagItems,
currentPage,
totalPages,
totalItems,
paginatedItems,
equipmentPage,
pillPage,
scrapPage,
totalEquipmentPages,
totalPillPages,
totalScrapPages,
equipmentItems,
pillItems,
scrapItems,
paginatedEquipment,
paginatedPills,
paginatedScraps,
usedCapacity,
loading,
error,
loadBag,
nextPage,
prevPage
nextEquipmentPage,
prevEquipmentPage,
nextPillPage,
prevPillPage,
nextScrapPage,
prevScrapPage
}
})

244
Build_God_Game/src/views/BagView.vue

@ -2,7 +2,6 @@
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'
@ -11,7 +10,6 @@ import itemDefaultIcon from '@/assets/images/item-default.svg'
const router = useRouter()
const bagStore = useBagStore()
const characterStore = useCharacterStore()
const bagName = computed(() => bagStore.characterBag?.bagName || '背包')
const bagCapacity = computed(() => bagStore.characterBag?.bagCapacity || 0)
@ -19,7 +17,8 @@ const usedCapacity = computed(() => bagStore.usedCapacity)
const itemTypeMap: Record<number, string> = {
1: '装备',
2: '丹药'
2: '丹药',
3: '垃圾'
}
const rarityMap: Record<number, string> = {
@ -29,10 +28,20 @@ const rarityMap: Record<number, string> = {
4: '传说'
}
const getItemTooltip = (item: { itemType: number; itemId: number; quantity: number; itemName: string | null; itemRarity: number | null }) => {
const type = itemTypeMap[item.itemType] || '未知'
const rarity = item.itemRarity ? rarityMap[item.itemRarity] || '' : ''
return `${item.itemName}\n类型: ${type}${rarity ? ` | 稀有度: ${rarity}` : ''}\n数量: ${item.quantity}`
const scrapLevelMap: Record<number, string> = {
1: '普通',
2: '优秀',
3: '精良',
4: '史诗',
5: '传说'
}
const scrapLevelColorMap: Record<number, string> = {
1: '#FFFFFF',
2: '#00FF00',
3: '#0077FF',
4: '#9932CC',
5: '#FF8C00'
}
const getItemIcon = (item: { icon: string | null }) => {
@ -42,6 +51,25 @@ const getItemIcon = (item: { icon: string | null }) => {
return itemDefaultIcon
}
const getEquipmentTooltip = (item: { itemName: string | null; itemRarity: number | null; quantity: number }) => {
const rarity = item.itemRarity ? rarityMap[item.itemRarity] || '' : ''
return `${item.itemName}\n类型: 装备${rarity ? ` | 稀有度: ${rarity}` : ''}\n数量: ${item.quantity}`
}
const getPillTooltip = (item: { itemName: string | null; quantity: number }) => {
return `${item.itemName}\n类型: 丹药\n数量: ${item.quantity}`
}
const getScrapTooltip = (item: any) => {
const level = item.scrapLevel ? scrapLevelMap[item.scrapLevel] || '' : ''
let stats = ''
if (item.attackBonus) stats += `\n攻击+${item.attackBonus}`
if (item.defenseBonus) stats += `\n防御+${item.defenseBonus}`
if (item.hpBonus) stats += `\n生命+${item.hpBonus}`
if (item.magicBonus) stats += `\n魔力+${item.magicBonus}`
return `${item.itemName}\n类型: 垃圾 | 稀有度: ${level}\n数量: ${item.quantity}${stats}`
}
const handleBack = () => {
router.push('/game')
}
@ -79,45 +107,133 @@ onMounted(() => {
{{ bagStore.error }}
</div>
<div v-else-if="bagStore.totalItems === 0" class="empty">
<div v-else-if="bagStore.bagItems.length === 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">
<img v-if="item.icon" :src="getItemIcon(item)" class="item-icon-img" :class="'rarity-' + (item.itemRarity || 1)" />
<div v-else 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 v-else class="bag-sections">
<!-- 装备区域 -->
<div class="section" v-if="bagStore.paginatedEquipment.length > 0 || bagStore.totalEquipmentPages > 0">
<div class="section-header">
<span class="section-title">装备</span>
<span class="section-count">({{ bagStore.paginatedEquipment.length }} / {{ bagStore.equipmentItems.length }})</span>
</div>
</el-tooltip>
</div>
<div class="items-grid">
<el-tooltip
v-for="item in bagStore.paginatedEquipment"
:key="item.id"
:content="getEquipmentTooltip(item)"
placement="top"
:show-after="300"
>
<div class="item-cell">
<img v-if="item.icon" :src="getItemIcon(item)" class="item-icon-img" :class="'rarity-' + (item.itemRarity || 1)" />
<div v-else class="item-icon"></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.totalEquipmentPages > 1" class="pagination">
<button
class="page-btn"
:disabled="bagStore.equipmentPage === 1"
@click="bagStore.prevEquipmentPage()"
>
上一页
</button>
<span class="page-info">{{ bagStore.equipmentPage }} / {{ bagStore.totalEquipmentPages }}</span>
<button
class="page-btn"
:disabled="bagStore.equipmentPage >= bagStore.totalEquipmentPages"
@click="bagStore.nextEquipmentPage()"
>
下一页
</button>
</div>
</div>
<!-- 丹药区域 -->
<div class="section" v-if="bagStore.paginatedPills.length > 0 || bagStore.totalPillPages > 0">
<div class="section-header">
<span class="section-title">丹药</span>
<span class="section-count">({{ bagStore.paginatedPills.length }} / {{ bagStore.pillItems.length }})</span>
</div>
<div class="items-grid">
<el-tooltip
v-for="item in bagStore.paginatedPills"
:key="item.id"
:content="getPillTooltip(item)"
placement="top"
:show-after="300"
>
<div class="item-cell">
<img v-if="item.icon" :src="getItemIcon(item)" class="item-icon-img" />
<div v-else class="item-icon">💊</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.totalPillPages > 1" class="pagination">
<button
class="page-btn"
:disabled="bagStore.pillPage === 1"
@click="bagStore.prevPillPage()"
>
上一页
</button>
<span class="page-info">{{ bagStore.pillPage }} / {{ bagStore.totalPillPages }}</span>
<button
class="page-btn"
:disabled="bagStore.pillPage >= bagStore.totalPillPages"
@click="bagStore.nextPillPage()"
>
下一页
</button>
</div>
</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 class="section" v-if="bagStore.paginatedScraps.length > 0 || bagStore.totalScrapPages > 0">
<div class="section-header">
<span class="section-title">垃圾</span>
<span class="section-count">({{ bagStore.paginatedScraps.length }} / {{ bagStore.scrapItems.length }})</span>
</div>
<div class="items-grid">
<el-tooltip
v-for="item in bagStore.paginatedScraps"
:key="item.id"
:content="getScrapTooltip(item)"
placement="top"
:show-after="300"
>
<div class="item-cell scrap-cell">
<img v-if="item.icon" :src="getItemIcon(item)" class="item-icon-img" :style="{ borderColor: scrapLevelColorMap[item.scrapLevel || 1] }" />
<div v-else class="item-icon" :style="{ color: scrapLevelColorMap[item.scrapLevel || 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.totalScrapPages > 1" class="pagination">
<button
class="page-btn"
:disabled="bagStore.scrapPage === 1"
@click="bagStore.prevScrapPage()"
>
上一页
</button>
<span class="page-info">{{ bagStore.scrapPage }} / {{ bagStore.totalScrapPages }}</span>
<button
class="page-btn"
:disabled="bagStore.scrapPage >= bagStore.totalScrapPages"
@click="bagStore.nextScrapPage()"
>
下一页
</button>
</div>
</div>
</div>
</div>
</div>
@ -209,11 +325,44 @@ onMounted(() => {
color: #ff4444;
}
.bag-sections {
display: flex;
flex-direction: column;
gap: 24px;
}
.section {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
padding: 16px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.section-title {
color: #ffffff;
font-size: 1rem;
font-weight: 500;
}
.section-count {
color: #666666;
font-size: 0.8rem;
}
.items-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
margin-bottom: 16px;
}
.item-cell {
@ -235,6 +384,10 @@ onMounted(() => {
border-color: rgba(255, 255, 255, 0.15);
}
.scrap-cell {
border: 1px solid rgba(255, 255, 255, 0.1);
}
.item-icon {
font-size: 2rem;
margin-bottom: 8px;
@ -245,6 +398,8 @@ onMounted(() => {
height: 2.5rem;
object-fit: contain;
margin-bottom: 8px;
border-radius: 4px;
border: 2px solid transparent;
}
.item-name {
@ -283,13 +438,14 @@ onMounted(() => {
}
.page-btn {
padding: 10px 20px;
padding: 8px 16px;
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;
font-size: 0.85rem;
}
.page-btn:hover:not(:disabled) {
@ -303,6 +459,6 @@ onMounted(() => {
.page-info {
color: #888888;
font-size: 0.9rem;
font-size: 0.85rem;
}
</style>
Loading…
Cancel
Save