2026-01-21 17:13:51 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="inventory-drawer">
|
|
|
|
|
|
<view class="drawer-header">
|
|
|
|
|
|
<text class="drawer-title">📦 背包</text>
|
|
|
|
|
|
<text class="drawer-close" @click="close">×</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<FilterTabs
|
|
|
|
|
|
:tabs="filterTabs"
|
|
|
|
|
|
:modelValue="currentFilter"
|
|
|
|
|
|
@update:modelValue="currentFilter = $event"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<scroll-view class="inventory-list" scroll-y>
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="item in filteredItems"
|
|
|
|
|
|
:key="item.uniqueId || item.id"
|
|
|
|
|
|
class="inventory-item"
|
|
|
|
|
|
:class="{ 'inventory-item--equipped': isEquipped(item) }"
|
|
|
|
|
|
@click="selectItem(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
|
|
|
|
|
|
<view class="inventory-item__info">
|
2026-01-23 16:20:10 +08:00
|
|
|
|
<text class="inventory-item__name" :style="{ color: item.qualityColor || '#fff' }">
|
2026-01-21 17:13:51 +08:00
|
|
|
|
{{ item.name }}
|
2026-01-23 16:20:10 +08:00
|
|
|
|
<text v-if="item.qualityName" class="quality-badge"> [{{ item.qualityName }}]</text>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
</text>
|
|
|
|
|
|
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text v-if="isEquipped(item)" class="inventory-item__equipped-badge">E</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="filteredItems.length === 0" class="inventory-empty">
|
|
|
|
|
|
<text class="inventory-empty__text">背包空空如也</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 物品详情 -->
|
|
|
|
|
|
<view v-if="selectedItem" class="item-detail">
|
|
|
|
|
|
<view class="item-detail__header">
|
|
|
|
|
|
<text class="item-detail__name">{{ selectedItem.name }}</text>
|
|
|
|
|
|
<text class="item-detail__close" @click="selectedItem = null">×</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="item-detail__desc">{{ selectedItem.description }}</text>
|
|
|
|
|
|
<text v-if="isEquipped(selectedItem)" class="item-detail__equipped">⚔️ 已装备</text>
|
|
|
|
|
|
<view class="item-detail__actions">
|
|
|
|
|
|
<TextButton
|
|
|
|
|
|
v-if="canEquip && !isEquipped(selectedItem)"
|
|
|
|
|
|
text="装备"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="equipItem"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<TextButton
|
|
|
|
|
|
v-if="canEquip && isEquipped(selectedItem)"
|
|
|
|
|
|
text="卸下"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
@click="unequipItem"
|
|
|
|
|
|
/>
|
2026-01-23 19:40:55 +08:00
|
|
|
|
<TextButton
|
|
|
|
|
|
v-if="canRead"
|
|
|
|
|
|
text="阅读"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
@click="readItem"
|
|
|
|
|
|
/>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
<TextButton
|
|
|
|
|
|
v-if="canUse"
|
|
|
|
|
|
text="使用"
|
|
|
|
|
|
@click="useItem"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<TextButton
|
|
|
|
|
|
text="出售"
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
@click="sellItem"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
import { usePlayerStore } from '@/store/player'
|
|
|
|
|
|
import { useGameStore } from '@/store/game'
|
2026-01-23 16:20:10 +08:00
|
|
|
|
import { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
|
2026-01-23 19:40:55 +08:00
|
|
|
|
import { startTask } from '@/utils/taskSystem.js'
|
2026-01-21 17:13:51 +08:00
|
|
|
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
|
|
|
|
|
import TextButton from '@/components/common/TextButton.vue'
|
|
|
|
|
|
|
|
|
|
|
|
const player = usePlayerStore()
|
|
|
|
|
|
const game = useGameStore()
|
|
|
|
|
|
|
|
|
|
|
|
const currentFilter = ref('all')
|
|
|
|
|
|
const selectedItem = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['close'])
|
|
|
|
|
|
|
|
|
|
|
|
const filterTabs = [
|
|
|
|
|
|
{ id: 'all', label: '全部' },
|
|
|
|
|
|
{ id: 'weapon', label: '武器' },
|
|
|
|
|
|
{ id: 'armor', label: '防具' },
|
|
|
|
|
|
{ id: 'consumable', label: '消耗品' },
|
|
|
|
|
|
{ id: 'material', label: '素材' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const filteredItems = computed(() => {
|
|
|
|
|
|
const items = player.inventory || []
|
|
|
|
|
|
if (currentFilter.value === 'all') {
|
|
|
|
|
|
return items
|
|
|
|
|
|
}
|
|
|
|
|
|
return items.filter(item => item.type === currentFilter.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const canEquip = computed(() => {
|
|
|
|
|
|
if (!selectedItem.value) return false
|
|
|
|
|
|
return ['weapon', 'armor', 'shield', 'accessory'].includes(selectedItem.value.type)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const canUse = computed(() => {
|
|
|
|
|
|
if (!selectedItem.value) return false
|
|
|
|
|
|
return selectedItem.value.type === 'consumable'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-23 19:40:55 +08:00
|
|
|
|
const canRead = computed(() => {
|
|
|
|
|
|
if (!selectedItem.value) return false
|
|
|
|
|
|
return selectedItem.value.type === 'book'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-21 17:13:51 +08:00
|
|
|
|
// 装备槽位映射
|
|
|
|
|
|
const slotMap = {
|
|
|
|
|
|
weapon: 'weapon',
|
|
|
|
|
|
armor: 'armor',
|
|
|
|
|
|
shield: 'shield',
|
|
|
|
|
|
accessory: 'accessory'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查物品是否已装备
|
|
|
|
|
|
function isEquipped(item) {
|
|
|
|
|
|
if (!item || !item.type) return false
|
|
|
|
|
|
const slot = slotMap[item.type]
|
|
|
|
|
|
if (!slot) return false
|
|
|
|
|
|
const equipped = player.equipment[slot]
|
|
|
|
|
|
// 通过 uniqueId 或 id 比较
|
|
|
|
|
|
return equipped && (equipped.uniqueId === item.uniqueId || equipped.id === item.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function close() {
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectItem(item) {
|
|
|
|
|
|
selectedItem.value = item
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function equipItem() {
|
|
|
|
|
|
if (!selectedItem.value) return
|
2026-01-23 16:20:10 +08:00
|
|
|
|
const result = equipItemUtil(player, game, selectedItem.value.uniqueId)
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
selectedItem.value = null
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function unequipItem() {
|
|
|
|
|
|
if (!selectedItem.value) return
|
|
|
|
|
|
const item = selectedItem.value
|
|
|
|
|
|
const slot = slotMap[item.type]
|
|
|
|
|
|
|
|
|
|
|
|
if (!slot) return
|
|
|
|
|
|
|
2026-01-23 16:20:10 +08:00
|
|
|
|
const result = unequipItemBySlot(player, game, slot)
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
selectedItem.value = null
|
|
|
|
|
|
}
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function useItem() {
|
|
|
|
|
|
if (!selectedItem.value) return
|
2026-01-23 16:20:10 +08:00
|
|
|
|
const result = useItemUtil(player, game, selectedItem.value.id, 1)
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
selectedItem.value = null
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 19:40:55 +08:00
|
|
|
|
function readItem() {
|
|
|
|
|
|
if (!selectedItem.value) return
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已经在阅读中
|
|
|
|
|
|
const existingTask = game.activeTasks?.find(t => t.type === 'reading')
|
|
|
|
|
|
if (existingTask) {
|
|
|
|
|
|
game.addLog('已经在阅读其他书籍了', 'error')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = startTask(game, player, 'reading', {
|
|
|
|
|
|
itemId: selectedItem.value.id,
|
|
|
|
|
|
duration: selectedItem.value.readingTime || 60
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
game.addLog(`开始阅读《${selectedItem.value.name}》`, 'info')
|
|
|
|
|
|
selectedItem.value = null
|
|
|
|
|
|
emit('close') // 关闭背包以便看到阅读进度
|
|
|
|
|
|
} else {
|
|
|
|
|
|
game.addLog(result.message, 'error')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 17:13:51 +08:00
|
|
|
|
function sellItem() {
|
|
|
|
|
|
if (!selectedItem.value) return
|
|
|
|
|
|
// 已装备的物品不能出售
|
|
|
|
|
|
if (isEquipped(selectedItem.value)) {
|
|
|
|
|
|
game.addLog('已装备的物品需要先卸下才能出售', 'error')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
game.addLog(`出售了 ${selectedItem.value.name}`, 'info')
|
|
|
|
|
|
// 出售逻辑待完善
|
|
|
|
|
|
selectedItem.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.inventory-drawer {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background-color: $bg-secondary;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
border-bottom: 1rpx solid $border-color;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-title {
|
|
|
|
|
|
color: $text-primary;
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-close {
|
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
padding: 0 16rpx;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
color: $text-primary;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inventory-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inventory-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
padding: 16rpx;
|
|
|
|
|
|
background-color: $bg-primary;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
background-color: $bg-tertiary;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&--equipped {
|
|
|
|
|
|
background-color: rgba($accent, 0.15);
|
|
|
|
|
|
border: 1rpx solid $accent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__icon {
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__name {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: $text-primary;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__count {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
margin-top: 4rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__equipped-badge {
|
|
|
|
|
|
width: 32rpx;
|
|
|
|
|
|
height: 32rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background-color: $accent;
|
|
|
|
|
|
color: $bg-primary;
|
|
|
|
|
|
font-size: 18rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 19:40:55 +08:00
|
|
|
|
.quality-badge {
|
|
|
|
|
|
font-size: 18rpx;
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
margin-left: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 17:13:51 +08:00
|
|
|
|
.inventory-empty {
|
|
|
|
|
|
padding: 80rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
&__text {
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.item-detail {
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
background-color: $bg-primary;
|
|
|
|
|
|
border-top: 1rpx solid $border-color;
|
|
|
|
|
|
|
|
|
|
|
|
&__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__name {
|
|
|
|
|
|
color: $text-primary;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__close {
|
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
padding: 0 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__desc {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__equipped {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 4rpx 12rpx;
|
|
|
|
|
|
background-color: rgba($accent, 0.2);
|
|
|
|
|
|
color: $accent;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
border-radius: 4rpx;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|