146 lines
6.4 KiB
Vue
146 lines
6.4 KiB
Vue
|
|
<template>
|
||
|
|
<div class="flex flex-col h-full gap-2 relative">
|
||
|
|
|
||
|
|
<!-- Top Bar: Gold & Title -->
|
||
|
|
<div class="flex items-center justify-between bg-[#1b1026] p-2 border border-[#f6b26b]">
|
||
|
|
<div class="flex items-center gap-2 text-[#9fd75b] font-bold tracking-widest">
|
||
|
|
<ShoppingBag :size="20" />
|
||
|
|
<span>商店 (SHOP)</span>
|
||
|
|
</div>
|
||
|
|
<div class="flex items-center gap-2 bg-[#2b193f] px-3 py-1 rounded border border-[#4a3b5e]">
|
||
|
|
<span class="text-[#99e550] text-sm uppercase">您的金幣:</span>
|
||
|
|
<span class="text-[#f6b26b] font-mono text-lg font-bold">{{ playerGold }}</span>
|
||
|
|
<Coins :size="16" class="text-[#f6b26b]" />
|
||
|
|
<div class="border-l border-[#4a3b5e] pl-2 ml-1">
|
||
|
|
<RefreshCw :size="14" class="text-[#8f80a0] cursor-pointer hover:text-white hover:rotate-180 transition-transform" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Main Action Tabs -->
|
||
|
|
<div class="flex gap-4 justify-center my-2">
|
||
|
|
<PixelButton
|
||
|
|
:variant="mode === 'BUY' ? 'primary' : 'secondary'"
|
||
|
|
@click="mode = 'BUY'"
|
||
|
|
class="w-32"
|
||
|
|
:class="{ 'bg-[#3d9e8f] border-[#2c7a6f]': mode === 'BUY' }"
|
||
|
|
:style="mode === 'BUY' ? { backgroundColor: '#3d9e8f', borderColor: '#2c7a6f' } : {}"
|
||
|
|
>
|
||
|
|
購買 (BUY)
|
||
|
|
</PixelButton>
|
||
|
|
<PixelButton
|
||
|
|
:variant="mode === 'SELL' ? 'primary' : 'secondary'"
|
||
|
|
@click="mode = 'SELL'"
|
||
|
|
class="w-32"
|
||
|
|
:style="mode === 'SELL' ? { backgroundColor: '#d95763', borderColor: '#ac3232', color: 'white' } : {}"
|
||
|
|
>
|
||
|
|
賣出 (SELL)
|
||
|
|
</PixelButton>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Category Filters -->
|
||
|
|
<div class="flex gap-1 overflow-x-auto pb-2 custom-scrollbar">
|
||
|
|
<button
|
||
|
|
v-for="cat in CATEGORY_FILTERS"
|
||
|
|
:key="cat.id"
|
||
|
|
@click="filter = cat.id"
|
||
|
|
class="flex items-center gap-1 px-3 py-1 text-xs border whitespace-nowrap transition-colors"
|
||
|
|
:class="filter === cat.id ? 'bg-[#9fd75b] text-[#1b1026] border-[#f6b26b]' : 'bg-[#150c1f] text-[#8f80a0] border-[#4a3b5e] hover:bg-[#2b193f]'"
|
||
|
|
>
|
||
|
|
<component :is="cat.icon" :size="12" />
|
||
|
|
{{ cat.label }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Items List -->
|
||
|
|
<div class="flex-grow bg-[#0f0816] border border-[#2b193f] p-2 overflow-y-auto custom-scrollbar">
|
||
|
|
<div v-if="displayedItems.length === 0" class="h-full flex items-center justify-center text-[#4a3b5e] flex-col gap-2">
|
||
|
|
<Search :size="32" />
|
||
|
|
<span>NO ITEMS FOUND</span>
|
||
|
|
</div>
|
||
|
|
<div v-else class="flex flex-col gap-2">
|
||
|
|
<div
|
||
|
|
v-for="item in displayedItems"
|
||
|
|
:key="item.id"
|
||
|
|
class="flex items-center justify-between p-2 bg-[#1b1026] border border-[#2b193f] hover:border-[#4a3b5e] transition-colors group"
|
||
|
|
>
|
||
|
|
<!-- Item Icon & Info -->
|
||
|
|
<div class="flex items-center gap-3">
|
||
|
|
<div class="w-10 h-10 bg-[#231533] border border-[#4a3b5e] flex items-center justify-center relative">
|
||
|
|
<!-- Simple Icon Logic -->
|
||
|
|
<Cookie v-if="item.category === ItemCategory.Food" color="#f6b26b" />
|
||
|
|
<Pill v-else-if="item.category === ItemCategory.Medicine" color="#d95763" />
|
||
|
|
<Sword v-else-if="item.category === ItemCategory.Equipment" color="#2ce8f4" />
|
||
|
|
<Gamepad2 v-else-if="item.category === ItemCategory.Toy" color="#99e550" />
|
||
|
|
<Gem v-else-if="item.category === ItemCategory.Accessory" color="#d584fb" />
|
||
|
|
|
||
|
|
<span v-if="item.quantity && item.quantity > 1" class="absolute bottom-0 right-0 bg-black text-white text-[9px] px-1">{{ item.quantity }}</span>
|
||
|
|
</div>
|
||
|
|
<div class="flex flex-col">
|
||
|
|
<span class="font-bold text-sm tracking-wide" :class="item.rarity === Rarity.Legendary ? 'text-[#ffa500]' : 'text-[#9fd75b]'">
|
||
|
|
{{ item.name }}
|
||
|
|
</span>
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<span class="text-[10px] text-[#f6b26b] font-mono">
|
||
|
|
$ {{ mode === 'SELL' ? Math.floor(item.price / 2) : item.price }}
|
||
|
|
</span>
|
||
|
|
<span v-if="item.quantity" class="text-[10px] text-[#8f80a0]">x {{ item.quantity }}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Action Button -->
|
||
|
|
<button
|
||
|
|
@click="mode === 'BUY' ? $emit('buy', item) : $emit('sell', item)"
|
||
|
|
class="px-4 py-1 border-2 text-xs font-bold tracking-widest active:translate-y-0.5"
|
||
|
|
:class="mode === 'BUY'
|
||
|
|
? 'bg-[#1b1026] border-[#9fd75b] text-[#9fd75b] hover:bg-[#9fd75b] hover:text-[#1b1026]'
|
||
|
|
: 'bg-[#1b1026] border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-[#1b1026]'"
|
||
|
|
>
|
||
|
|
{{ mode === 'BUY' ? '購買' : '賣出' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, computed } from 'vue';
|
||
|
|
import { ShoppingBag, Coins, Filter, Cookie, Pill, Sword, Gamepad2, Gem, Search, RefreshCw } from 'lucide-vue-next';
|
||
|
|
import PixelButton from './PixelButton.vue';
|
||
|
|
import { ItemCategory, Rarity } from '~/types/pixel';
|
||
|
|
import type { Item } from '~/types/pixel';
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
playerGold: number;
|
||
|
|
inventory: Item[];
|
||
|
|
shopItems: Item[];
|
||
|
|
}
|
||
|
|
|
||
|
|
const props = defineProps<Props>();
|
||
|
|
defineEmits(['buy', 'sell']);
|
||
|
|
|
||
|
|
const mode = ref<'BUY' | 'SELL'>('BUY');
|
||
|
|
const filter = ref<string>('ALL');
|
||
|
|
|
||
|
|
const CATEGORY_FILTERS = [
|
||
|
|
{ id: 'ALL', label: '全部 (ALL)', icon: Search },
|
||
|
|
{ id: ItemCategory.Food, label: '食物', icon: Cookie },
|
||
|
|
{ id: ItemCategory.Medicine, label: '藥品', icon: Pill },
|
||
|
|
{ id: ItemCategory.Equipment, label: '裝備', icon: Sword },
|
||
|
|
{ id: ItemCategory.Toy, label: '玩具', icon: Gamepad2 },
|
||
|
|
{ id: ItemCategory.Accessory, label: '飾品', icon: Gem },
|
||
|
|
];
|
||
|
|
|
||
|
|
const displayedItems = computed(() => {
|
||
|
|
const source = mode.value === 'BUY' ? props.shopItems : props.inventory;
|
||
|
|
return source.filter(item => {
|
||
|
|
if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items
|
||
|
|
if (filter.value === 'ALL') return true;
|
||
|
|
return item.category === filter.value;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|