pet_data/app/components/pixel/InventoryOverlay.vue

274 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="flex flex-col h-full gap-2">
<!-- 1. 已装备装备栏 -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
<div v-for="(slotConfig, key) in EQUIPMENT_SLOTS" :key="key" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2">
<!-- 装备槽标题 -->
<div class="flex items-center gap-2 justify-center border-b border-[#2b193f] pb-1">
<component :is="SLOT_ICONS[key]" :size="14" class="text-[#8f80a0]" />
<span class="text-[#2ce8f4] text-xs font-bold uppercase">{{ slotConfig.name }}</span>
</div>
<!-- 实际装备槽 -->
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center">
<span class="text-[9px] text-[#8f80a0] mb-1">装备</span>
<div v-if="getEquippedItem(key, false)" class="flex flex-col items-center gap-1">
<div class="relative w-8 h-8">
<PixelItemIcon :category="getEquippedItem(key, false)!.category" :color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color" />
</div>
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">{{ getEquippedItem(key, false)!.name }}</span>
</div>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
</div>
<!-- 外观槽 -->
<div class="bg-[#150c1f] border border-[#4a3b5e] p-2 min-h-[40px] flex flex-col items-center justify-center">
<span class="text-[9px] text-[#8f80a0] mb-1">外觀</span>
<div v-if="getEquippedItem(key, true)" class="flex flex-col items-center gap-1">
<div class="relative w-8 h-8">
<PixelItemIcon :category="getEquippedItem(key, true)!.category" :color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color" />
</div>
<span class="text-xs text-center truncate w-full px-1 font-bold" :style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">{{ getEquippedItem(key, true)!.name }}</span>
</div>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
</div>
</div>
</div>
<!-- 2. 背包区域 - 全宽 + 固定高度 + 滚动 -->
<div class="flex-grow overflow-hidden mt-2">
<PixelFrame class="h-full flex flex-col bg-[#1b1026]" :title="`背包 (${items.filter(i => !i.isEquipped).length})`">
<div class="overflow-y-auto p-3 custom-scrollbar" style="max-height: calc(100vh - 400px);">
<div class="grid grid-cols-6 gap-2">
<!-- 30个格子6x5 -->
<div v-for="index in 30" :key="index" class="relative aspect-square">
<!-- 有物品:显示物品 + 悬浮卡片 -->
<template v-if="getInventorySlotItem(index - 1)">
<button class="w-full h-full p-2 flex flex-col items-center justify-center gap-1 border-2 transition-all group rounded-sm border-[#4a3b5e] hover:border-[#8f80a0] hover:bg-[#321e4a] bg-[#2b193f]">
<div class="relative w-10 h-10">
<PixelItemIcon
:category="getInventorySlotItem(index - 1).category"
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
/>
<!-- 稀有度发光 -->
<div
v-if="['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)"
class="absolute inset-0 rounded-full opacity-40 blur-md pointer-events-none"
:style="{ backgroundColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }"
></div>
<span
v-if="getInventorySlotItem(index - 1).quantity && getInventorySlotItem(index - 1).quantity > 1"
class="absolute -bottom-1 -right-1 text-[9px] bg-black text-white px-1 border border-[#4a3b5e] rounded-sm z-10 shadow-sm font-mono"
>{{ getInventorySlotItem(index - 1).quantity }}</span>
</div>
<span
class="text-[10px] text-center leading-tight line-clamp-2 w-full font-medium relative z-10"
:style="{
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
textShadow: ['rare', 'epic', 'legendary'].includes(getInventorySlotItem(index - 1).rarity)
? `0 0 3px ${ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color}80`
: 'none'
}"
>{{ getInventorySlotItem(index - 1).name }}</span>
<!-- 详细悬浮卡片 -->
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity z-50 w-72 text-left max-h-[400px] overflow-y-auto custom-scrollbar">
<!-- 物品头部 -->
<div class="flex gap-3 mb-3 pb-3 border-b border-[#4a3b5e]">
<div class="w-16 h-16 bg-[#1b1026] border-2 rounded flex items-center justify-center p-2 flex-shrink-0"
:style="{ borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
<PixelItemIcon
:category="getInventorySlotItem(index - 1).category"
:color="ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color }">
{{ getInventorySlotItem(index - 1).name }}
</div>
<div class="flex gap-2 text-[10px] flex-wrap">
<span class="px-2 py-0.5 bg-[#1b1026] border border-[#4a3b5e] rounded text-[#8f80a0]">
{{ getItemTypeName(getInventorySlotItem(index - 1).type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color,
color: ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.color
}">
{{ ITEM_RARITY[getInventorySlotItem(index - 1).rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getInventorySlotItem(index - 1).description }}"
</p>
<!-- 效果 -->
<div v-if="getInventorySlotItem(index - 1).effects" class="mb-3 space-y-1">
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
<div v-if="getInventorySlotItem(index - 1).effects.flat" class="space-y-1">
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.flat" :key="key"
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
<span class="font-mono font-bold">+{{ val }}</span>
</div>
</div>
<div v-if="getInventorySlotItem(index - 1).effects.percent" class="space-y-1 mt-1">
<div v-for="(val, key) in getInventorySlotItem(index - 1).effects.percent" :key="key"
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(key) }}</span>
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 耐久度 -->
<div v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity" class="mb-3">
<div class="flex justify-between text-[10px] text-[#8f80a0] mb-1">
<span>耐久度</span>
<span class="font-mono">{{ getInventorySlotItem(index - 1).durability }} / {{ getInventorySlotItem(index - 1).maxDurability }}</span>
</div>
<div class="h-1.5 w-full bg-[#1b1026] border border-[#4a3b5e] rounded-full overflow-hidden">
<div class="h-full bg-[#f6b26b] transition-all"
:style="{ width: `${(getInventorySlotItem(index - 1).durability / getInventorySlotItem(index - 1).maxDurability) * 100}%` }">
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<!-- 装备按钮 -->
<button
v-if="getInventorySlotItem(index - 1).slot && getInventorySlotItem(index - 1).type !== ItemType.Appearance"
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, false)"
class="w-full px-3 py-2 bg-[#9fd75b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#b5e87b] transition-colors"
>
裝備
</button>
<!-- 外观按钮 -->
<button
v-if="getInventorySlotItem(index - 1).type === ItemType.Appearance"
@click.stop="$emit('equip', getInventorySlotItem(index - 1).id, true)"
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
>
裝備外觀
</button>
<!-- 使用按钮 -->
<button
v-if="getInventorySlotItem(index - 1).type === ItemType.Consumable"
@click.stop="$emit('use', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#2ce8f4] text-[#1b1026] rounded text-xs font-bold hover:bg-[#5cf4ff] transition-colors"
>
使用物品
</button>
<!-- 修理按钮 -->
<button
v-if="getInventorySlotItem(index - 1).maxDurability && getInventorySlotItem(index - 1).maxDurability !== Infinity && getInventorySlotItem(index - 1).durability < getInventorySlotItem(index - 1).maxDurability"
@click.stop="$emit('repair', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#f6b26b] text-[#1b1026] rounded text-xs font-bold hover:bg-[#ffc68b] transition-colors flex items-center justify-center gap-2"
>
<Hammer :size="12" />
修理 ({{ Math.ceil((getInventorySlotItem(index - 1).maxDurability - getInventorySlotItem(index - 1).durability) * 0.5) }} G)
</button>
<!-- 删除按钮 -->
<button
@click.stop="$emit('delete', getInventorySlotItem(index - 1).id)"
class="w-full px-3 py-2 bg-[#d95763] text-white rounded text-xs font-bold hover:bg-[#ff6b7a] transition-colors flex items-center justify-center gap-2"
>
<Trash2 :size="12" />
丟棄
</button>
</div>
</div>
</button>
</template>
<!-- 空格子:占位符 -->
<div
v-else
class="w-full h-full border-2 border-[#2b193f] bg-[#150c1f] rounded-sm flex items-center justify-center opacity-30"
>
<div class="w-8 h-8 border border-[#2b193f] rounded opacity-20"></div>
</div>
</div>
</div>
</div>
</PixelFrame>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Sword, Shield, Crown, Gem, Sparkles, Star, HelpCircle, Trash2, X, Hammer } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
import PixelItemIcon from './PixelItemIcon.vue';
import { formatBuffKey } from '../../utils/formatters.js';
import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS, ITEM_TYPES } from '../../../../data/items.js';
// Use any types since we're using data directly
type Item = any;
interface Props {
items: Item[];
}
const props = defineProps<Props>();
defineEmits(['equip', 'unequip', 'use', 'delete', 'repair']);
const selectedItemId = ref<string | null>(null);
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
const setSelectedItemId = (id: string | undefined) => {
if (id) selectedItemId.value = id;
};
// Map data constants to local helpers for template compatibility
const ItemType = {
Equipment: ITEM_TYPE.EQUIPMENT,
Consumable: ITEM_TYPE.CONSUMABLE,
Talisman: ITEM_TYPE.TALISMAN,
Special: ITEM_TYPE.SPECIAL,
Appearance: ITEM_TYPE.APPEARANCE
};
const SLOT_ICONS: Record<string, any> = {
weapon: Sword,
armor: Shield,
hat: Crown,
accessory: Gem,
talisman: Sparkles,
special: Star,
};
const getEquippedItem = (slot: string, isAppearance: boolean) => {
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
};
const getInventorySlotItem = (index: number) => {
const unequippedItems = props.items.filter(i => !i.isEquipped);
return unequippedItems[index] || null;
};
const getItemTypeName = (type: string) => {
return ITEM_TYPES[type]?.name || type;
};
</script>