2025-11-26 06:53:44 +00:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="flex flex-col h-full gap-2">
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
<!-- 1. 已装备装备栏 -->
|
2025-11-26 06:53:44 +00:00
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
2025-11-26 15:31:46 +00:00
|
|
|
|
<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>
|
2025-11-26 06:53:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
<!-- 实际装备槽 -->
|
|
|
|
|
|
<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>
|
2025-11-26 06:53:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
<!-- 外观槽 -->
|
|
|
|
|
|
<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>
|
2025-11-26 06:53:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
<!-- 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>
|
2025-11-26 06:53:44 +00:00
|
|
|
|
</div>
|
2025-11-26 15:31:46 +00:00
|
|
|
|
</div>
|
2025-11-26 06:53:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PixelFrame>
|
|
|
|
|
|
</div>
|
2025-11-26 15:31:46 +00:00
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, computed } from 'vue';
|
2025-11-26 15:31:46 +00:00
|
|
|
|
import { Sword, Shield, Crown, Gem, Sparkles, Star, HelpCircle, Trash2, X, Hammer } from 'lucide-vue-next';
|
2025-11-26 06:53:44 +00:00
|
|
|
|
import PixelFrame from './PixelFrame.vue';
|
|
|
|
|
|
import PixelButton from './PixelButton.vue';
|
2025-11-26 15:31:46 +00:00
|
|
|
|
import PixelItemIcon from './PixelItemIcon.vue';
|
|
|
|
|
|
import { formatBuffKey } from '../../utils/formatters.js';
|
2025-11-26 09:53:03 +00:00
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS, ITEM_TYPES } from '../../../../data/items.js';
|
2025-11-26 09:53:03 +00:00
|
|
|
|
|
|
|
|
|
|
// Use any types since we're using data directly
|
|
|
|
|
|
type Item = any;
|
2025-11-26 06:53:44 +00:00
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
items: Item[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<Props>();
|
2025-11-26 15:31:46 +00:00
|
|
|
|
defineEmits(['equip', 'unequip', 'use', 'delete', 'repair']);
|
2025-11-26 06:53:44 +00:00
|
|
|
|
|
|
|
|
|
|
const selectedItemId = ref<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
const setSelectedItemId = (id: string | undefined) => {
|
|
|
|
|
|
if (id) selectedItemId.value = id;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
const SLOT_ICONS: Record<string, any> = {
|
|
|
|
|
|
weapon: Sword,
|
|
|
|
|
|
armor: Shield,
|
|
|
|
|
|
hat: Crown,
|
|
|
|
|
|
accessory: Gem,
|
|
|
|
|
|
talisman: Sparkles,
|
|
|
|
|
|
special: Star,
|
2025-11-26 09:53:03 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
const getEquippedItem = (slot: string, isAppearance: boolean) => {
|
|
|
|
|
|
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
|
2025-11-26 06:53:44 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
const getInventorySlotItem = (index: number) => {
|
|
|
|
|
|
const unequippedItems = props.items.filter(i => !i.isEquipped);
|
|
|
|
|
|
return unequippedItems[index] || null;
|
2025-11-26 06:53:44 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-26 15:31:46 +00:00
|
|
|
|
const getItemTypeName = (type: string) => {
|
|
|
|
|
|
return ITEM_TYPES[type]?.name || type;
|
2025-11-26 06:53:44 +00:00
|
|
|
|
};
|
|
|
|
|
|
</script>
|