pet_data/app/components/pixel/InventoryOverlay.vue

493 lines
28 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 overflow-visible">
<!-- 1. 已装备装备栏 -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 overflow-visible">
<div v-for="(slotConfig, key) in EQUIPMENT_SLOTS" :key="key" class="border border-[#4a3b5e] bg-[#0f0816] p-2 flex flex-col gap-2 overflow-visible">
<!-- 装备槽标题 -->
<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 relative">
<span class="text-[9px] text-[#8f80a0] mb-1">装备</span>
<button
v-if="getEquippedItem(key, false)"
class="flex flex-col items-center gap-1 hover:bg-[#321e4a] transition-all p-1 rounded w-full"
@mouseenter="onEquippedItemMouseEnter(`${key}-equipment`)"
@mouseleave="onItemMouseLeave"
>
<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
v-show="hoveredEquippedSlot === `${key}-equipment`"
@mouseenter="onTooltipMouseEnter"
@mouseleave="onTooltipMouseLeave"
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] 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[getEquippedItem(key, false)!.rarity]?.color }">
<PixelItemIcon
:category="getEquippedItem(key, false)!.category"
:color="ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color }">
{{ getEquippedItem(key, false)!.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(getEquippedItem(key, false)!.type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color,
color: ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.color
}">
{{ ITEM_RARITY[getEquippedItem(key, false)!.rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getEquippedItem(key, false)!.description }}"
</p>
<!-- 效果 -->
<div v-if="getEquippedItem(key, false)!.effects" class="mb-3 space-y-1">
<div class="text-[10px] text-[#99e550] font-bold mb-1">效果</div>
<div v-if="getEquippedItem(key, false)!.effects.flat" class="space-y-1">
<div v-for="(val, effectKey) in getEquippedItem(key, false)!.effects.flat" :key="effectKey"
class="text-[10px] text-[#9fd75b] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(effectKey) }}</span>
<span class="font-mono font-bold">+{{ val }}</span>
</div>
</div>
<div v-if="getEquippedItem(key, false)!.effects.percent" class="space-y-1 mt-1">
<div v-for="(val, effectKey) in getEquippedItem(key, false)!.effects.percent" :key="effectKey"
class="text-[10px] text-[#2ce8f4] flex justify-between border-b border-[#2b193f] border-dashed pb-1">
<span class="text-[#8f80a0]">{{ formatBuffKey(effectKey) }}</span>
<span class="font-mono font-bold">+{{ (val * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
<!-- 卸下按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<button
@click.stop="$emit('unequip', key, false)"
class="w-full px-3 py-2 bg-[#8f80a0] text-[#1b1026] rounded text-xs font-bold hover:bg-[#a598b8] transition-colors"
>
卸下裝備
</button>
</div>
</div>
</button>
<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 relative">
<span class="text-[9px] text-[#8f80a0] mb-1">外觀</span>
<button
v-if="getEquippedItem(key, true)"
class="flex flex-col items-center gap-1 hover:bg-[#321e4a] transition-all p-1 rounded w-full"
@mouseenter="onEquippedItemMouseEnter(`${key}-appearance`)"
@mouseleave="onItemMouseLeave"
>
<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
v-show="hoveredEquippedSlot === `${key}-appearance`"
@mouseenter="onTooltipMouseEnter"
@mouseleave="onTooltipMouseLeave"
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] 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[getEquippedItem(key, true)!.rarity]?.color }">
<PixelItemIcon
:category="getEquippedItem(key, true)!.category"
:color="ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color"
/>
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold mb-1"
:style="{ color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color }">
{{ getEquippedItem(key, true)!.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(getEquippedItem(key, true)!.type) }}
</span>
<span class="px-2 py-0.5 border rounded font-bold"
:style="{
borderColor: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color,
color: ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.color
}">
{{ ITEM_RARITY[getEquippedItem(key, true)!.rarity]?.name }}
</span>
</div>
</div>
</div>
<!-- 描述 -->
<p class="text-[11px] text-[#e0d8f0] italic mb-3 leading-relaxed">
"{{ getEquippedItem(key, true)!.description }}"
</p>
<!-- 卸下按钮 -->
<div class="space-y-2 pt-2 border-t border-[#4a3b5e]">
<button
@click.stop="$emit('unequip', key, true)"
class="w-full px-3 py-2 bg-[#d584fb] text-[#1b1026] rounded text-xs font-bold hover:bg-[#e5a4ff] transition-colors"
>
卸下外觀
</button>
</div>
</div>
</button>
<span v-else class="text-[10px] text-[#4a3b5e] z-10 opacity-50">空</span>
</div>
</div>
</div>
<!-- 2. 背包区域 - 固定高度 + 滚动 -->
<div class="flex-grow overflow-visible mt-2">
<PixelFrame class="h-full flex flex-col bg-[#1b1026]" :title="`背包 (${items.filter(i => !i.isEquipped).length})`">
<div class="overflow-y-auto overflow-x-visible p-3 custom-scrollbar" style="height: 400px;">
<div class="grid grid-cols-6 gap-2">
<!-- 动态格子数量基础30格有道具时自动扩展 -->
<div v-for="index in totalSlots" :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]"
@mouseenter="onItemMouseEnter(index - 1)"
@mouseleave="onItemMouseLeave"
>
<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
v-show="hoveredItemIndex === index - 1"
@mouseenter="onTooltipMouseEnter"
@mouseleave="onTooltipMouseLeave"
class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-4 py-3 bg-[#0f0816] border-2 border-[#4a3b5e] rounded-sm shadow-xl pointer-events-auto z-[100] 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 hoveredItemIndex = ref<number | null>(null);
const hoveredEquippedSlot = ref<string | null>(null); // 新增:用於追蹤裝備槽的懸浮狀態
const isHoveringTooltip = ref<boolean>(false);
const hideTooltipTimeout = ref<number | null>(null);
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
// 计算总格子数量基础30格有道具时自动扩展
const totalSlots = computed(() => {
const unequippedItems = props.items.filter(i => !i.isEquipped);
const minSlots = 30;
const itemCount = unequippedItems.length;
// 如果道具數量超過30則自動擴展到下一個6的倍數
if (itemCount > minSlots) {
return Math.ceil(itemCount / 6) * 6;
}
return minSlots;
});
const setSelectedItemId = (id: string | undefined) => {
if (id) selectedItemId.value = id;
};
// 處理裝備槽的滑鼠進入事件
const onEquippedItemMouseEnter = (slotKey: string) => {
// 清除任何待處理的隱藏 timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
// 設置當前懸停的裝備槽
hoveredEquippedSlot.value = slotKey;
// 清除背包道具的懸停狀態
hoveredItemIndex.value = null;
};
// 處理背包道具的滑鼠進入事件
const onItemMouseEnter = (itemIndex: number) => {
// 清除任何待處理的隱藏 timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
// 設置當前懸停的道具索引
hoveredItemIndex.value = itemIndex;
// 清除裝備槽的懸停狀態
hoveredEquippedSlot.value = null;
};
const onItemMouseLeave = () => {
// 清除之前的timeout如果有
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
}
// 延遲較長時間再檢查給使用者足夠時間移動到tooltip上
hideTooltipTimeout.value = setTimeout(() => {
if (!isHoveringTooltip.value) {
hoveredItemIndex.value = null;
hoveredEquippedSlot.value = null;
}
}, 300); // 增加到 300ms
};
const onTooltipMouseEnter = () => {
// 取消隱藏的timeout
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
isHoveringTooltip.value = true;
};
const onTooltipMouseLeave = () => {
// 滑鼠離開 tooltip立即隱藏
isHoveringTooltip.value = false;
hoveredItemIndex.value = null;
hoveredEquippedSlot.value = null;
if (hideTooltipTimeout.value) {
clearTimeout(hideTooltipTimeout.value);
hideTooltipTimeout.value = null;
}
};
// 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>