Files
text-adventure-game/components/drawers/ShopDrawer.vue
Claude cef974d94f feat: 优化游戏体验和系统平衡性
- 修复商店物品名称显示问题,添加堆叠物品出售数量选择
- 自动战斗状态持久化,战斗结束显示"寻找中"状态
- 战斗日志显示经验获取详情(战斗经验、武器经验)
- 技能进度条显示当前/最大经验值
- 阅读自动解锁技能并持续获得阅读经验,背包可直接阅读
- 优化训练平衡:时长60秒,经验5点/秒,耐力消耗降低
- 实现自然回复系统:基于体质回复HP/耐力,休息提供3倍加成
- 战斗和训练时不进行自然回复

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 19:40:55 +08:00

785 lines
19 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>
<view class="shop-drawer">
<view class="drawer-header">
<view class="header-left">
<text class="drawer-title">{{ shopConfig.icon }} {{ shopConfig.name }}</text>
<text class="drawer-owner">{{ shopConfig.owner }}</text>
</view>
<text class="drawer-close" @click="close">×</text>
</view>
<!-- 玩家货币显示 -->
<view class="currency-display">
<text class="currency-label">你的金币:</text>
<text class="currency-value">{{ formatCurrency(player.currency.copper) }}</text>
</view>
<!-- /卖切换 -->
<FilterTabs
:tabs="modeTabs"
:modelValue="currentMode"
@update:modelValue="currentMode = $event"
/>
<!-- 批量操作栏仅出售模式 -->
<view v-if="currentMode === 'sell'" class="bulk-actions">
<view class="bulk-actions__left">
<text class="bulk-actions__count">已选: {{ selectedItems.length }}</text>
<text class="bulk-actions__total">总价: {{ totalSellPrice }}</text>
</view>
<view class="bulk-actions__right">
<text
v-if="!bulkMode"
class="bulk-actions__link"
@click="bulkMode = true"
>
批量出售
</text>
<template v-else>
<text class="bulk-actions__link" @click="selectAll">全选</text>
<text class="bulk-actions__link" @click="cancelBulk">取消</text>
</template>
</view>
</view>
<!-- 购买列表 -->
<scroll-view v-if="currentMode === 'buy'" class="shop-list" scroll-y>
<view
v-for="item in shopItems"
:key="item.id"
class="shop-item"
:class="{ 'shop-item--disabled': !canBuy(item) }"
@click="tryBuy(item)"
>
<view class="shop-item__main">
<text class="shop-item__icon">{{ item.icon || '📦' }}</text>
<view class="shop-item__info">
<text class="shop-item__name">{{ item.name }}</text>
<text class="shop-item__desc">{{ item.description }}</text>
</view>
</view>
<view class="shop-item__right">
<text class="shop-item__price">{{ getBuyPrice(item.id) }}</text>
<text v-if="item.stock > 0" class="shop-item__stock">库存:{{ item.stock }}</text>
<text v-else class="shop-item__stock">无限</text>
</view>
</view>
<view v-if="shopItems.length === 0" class="shop-empty">
<text class="shop-empty__text">商店空空如也</text>
</view>
</scroll-view>
<!-- 出售列表 -->
<scroll-view v-else class="shop-list" scroll-y>
<view
v-for="item in sellableItems"
:key="item.uniqueId || item.id"
class="shop-item"
:class="{
'shop-item--selected': isItemSelected(item),
'shop-item--bulk': bulkMode
}"
@click="handleSellItemClick(item)"
>
<!-- 多选复选框 -->
<view v-if="bulkMode" class="shop-item__checkbox">
<text v-if="isItemSelected(item)" class="checkbox-icon"></text>
</view>
<view class="shop-item__main">
<text class="shop-item__icon">{{ item.icon || '📦' }}</text>
<view class="shop-item__info">
<text class="shop-item__name">{{ item.name }}</text>
<text v-if="item.count > 1" class="shop-item__count">x{{ item.count }}</text>
</view>
</view>
<text class="shop-item__price">{{ getSellPrice(item) }}</text>
</view>
<view v-if="sellableItems.length === 0" class="shop-empty">
<text class="shop-empty__text">没有可出售的物品</text>
</view>
</scroll-view>
<!-- 批量出售按钮 -->
<view v-if="bulkMode && selectedItems.length > 0" class="bulk-confirm-bar">
<text class="bulk-confirm-text">出售 {{ selectedItems.length }} 件物品获得 {{ totalSellPrice }} 铜币</text>
<TextButton text="确认出售" type="primary" @click="confirmBulkSell" />
</view>
<!-- 物品详情弹窗 -->
<view v-if="selectedItem" class="item-detail-popup" @click="selectedItem = null">
<view class="item-detail" @click.stop>
<text class="item-detail__name">{{ selectedItem.name }}</text>
<text class="item-detail__desc">{{ selectedItem.description }}</text>
<view class="item-detail__actions">
<TextButton
v-if="currentMode === 'buy' && selectedItem.buyItem"
text="购买"
type="primary"
@click="confirmBuy"
/>
<TextButton
v-if="currentMode === 'sell' && selectedItem.sellItem"
text="出售"
type="primary"
@click="confirmSell"
/>
<TextButton
text="取消"
@click="selectedItem = null"
/>
</view>
</view>
</view>
<!-- 数量选择弹窗 -->
<view v-if="quantitySelector" class="item-detail-popup" @click="cancelQuantitySell">
<view class="item-detail" @click.stop>
<text class="item-detail__name">选择出售数量</text>
<view class="quantity-selector">
<text class="quantity-selector__item-name">{{ quantitySelector.item.icon }} {{ quantitySelector.item.name }}</text>
<text class="quantity-selector__available">拥有: {{ quantitySelector.maxCount }}</text>
<view class="quantity-selector__controls">
<TextButton text="-" type="default" @click="decreaseSellCount" :disabled="quantitySelector.selectedCount <= 1" />
<text class="quantity-selector__count">{{ quantitySelector.selectedCount }}</text>
<TextButton text="+" type="default" @click="increaseSellCount" :disabled="quantitySelector.selectedCount >= quantitySelector.maxCount" />
</view>
<TextButton text="全部" type="default" size="small" @click="setMaxSellCount" style="margin-top: 12rpx;" />
<view class="quantity-selector__preview">
<text class="quantity-selector__price-label">预计获得:</text>
<text class="quantity-selector__price">{{ getSellPrice(quantitySelector.item) * quantitySelector.selectedCount }} 铜币</text>
</view>
</view>
<view class="item-detail__actions">
<TextButton text="确认出售" type="primary" @click="confirmQuantitySell" />
<TextButton text="取消" @click="cancelQuantitySell" />
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
import { getShopConfig, getBuyPrice as calcBuyPrice, getSellPrice as calcSellPrice } from '@/config/shop.js'
import { ITEM_CONFIG } from '@/config/items.js'
import { addItemToInventory, removeItemFromInventory } from '@/utils/itemSystem.js'
import FilterTabs from '@/components/common/FilterTabs.vue'
import TextButton from '@/components/common/TextButton.vue'
const player = usePlayerStore()
const game = useGameStore()
const currentMode = ref('buy')
const selectedItem = ref(null)
const bulkMode = ref(false)
const selectedItems = ref([])
// 数量选择弹窗状态
const quantitySelector = ref(null) // { item: Object, maxCount: Number, selectedCount: Number }
const emit = defineEmits(['close'])
const modeTabs = [
{ id: 'buy', label: '购买' },
{ id: 'sell', label: '出售' }
]
// 获取当前商店配置
const shopConfig = computed(() => {
const config = getShopConfig(player.currentLocation)
return config || { name: '未知商店', owner: '未知', icon: '🏪', items: [] }
})
// 商店可售物品
const shopItems = computed(() => {
const config = shopConfig.value
if (!config.items) return []
return config.items
.filter(item => item.stock === -1 || item.stock > 0)
.map(shopItem => {
// 从 ITEM_CONFIG 获取完整物品信息
const itemConfig = ITEM_CONFIG[shopItem.itemId]
if (!itemConfig) return null
return {
...itemConfig, // 复制所有物品属性name, icon, description等
id: shopItem.itemId, // 使用 itemId 作为 id
stock: shopItem.stock,
baseStock: shopItem.baseStock
}
})
.filter(Boolean)
})
// 玩家可出售物品
const sellableItems = computed(() => {
return (player.inventory || []).filter(item => {
// 关键道具不能出售
if (item.type === 'key') return false
return true
})
})
// 计算选中物品总价
const totalSellPrice = computed(() => {
return selectedItems.value.reduce((total, item) => {
return total + getSellPrice(item) * item.count
}, 0)
})
function formatCurrency(copper) {
if (copper >= 10000) {
const gold = Math.floor(copper / 10000)
const silver = Math.floor((copper % 10000) / 100)
const remaining = copper % 100
return `${gold}${silver}${remaining}`
} else if (copper >= 100) {
const silver = Math.floor(copper / 100)
const remaining = copper % 100
return `${silver}${remaining}`
}
return `${copper}`
}
function getBuyPrice(itemId) {
return calcBuyPrice(game, itemId)
}
function getSellPrice(item) {
return calcSellPrice(game, item.id, item.quality || 100)
}
function canBuy(item) {
const price = getBuyPrice(item.id)
return player.currency.copper >= price
}
function tryBuy(item) {
if (!canBuy(item)) {
game.addLog('金币不足!', 'error')
return
}
selectedItem.value = {
...item,
buyItem: true
}
}
function trySell(item) {
selectedItem.value = {
...item,
sellItem: true
}
}
// 检查物品是否被选中
function isItemSelected(item) {
return selectedItems.value.some(i =>
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
)
}
// 处理出售列表点击
function handleSellItemClick(item) {
if (bulkMode.value) {
// 批量模式:切换选中状态(批量出售出售全部)
const index = selectedItems.value.findIndex(i =>
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
)
if (index > -1) {
selectedItems.value.splice(index, 1)
} else {
selectedItems.value.push(item)
}
} else {
// 普通模式:检查是否是堆叠物品
if (item.count > 1) {
// 堆叠物品:显示数量选择弹窗
quantitySelector.value = {
item: item,
maxCount: item.count,
selectedCount: 1
}
} else {
// 非堆叠物品:直接显示详情
trySell(item)
}
}
}
// 全选
function selectAll() {
selectedItems.value = [...sellableItems.value]
}
// 取消批量模式
function cancelBulk() {
bulkMode.value = false
selectedItems.value = []
}
// 批量出售
function confirmBulkSell() {
if (selectedItems.value.length === 0) return
let totalEarned = 0
const itemsToRemove = []
for (const item of selectedItems.value) {
const price = getSellPrice(item) * item.count
totalEarned += price
itemsToRemove.push(item)
}
// 移除所有选中的物品
for (const item of itemsToRemove) {
removeItemFromInventory(player, item.uniqueId || item.id)
}
// 增加金币
player.currency.copper += totalEarned
game.addLog(`批量出售了 ${itemsToRemove.length} 件物品,获得 ${totalEarned} 铜币`, 'reward')
// 重置选择
bulkMode.value = false
selectedItems.value = []
}
function confirmBuy() {
if (!selectedItem.value) return
const item = selectedItem.value
const price = getBuyPrice(item.id)
if (player.currency.copper < price) {
game.addLog('金币不足!', 'error')
return
}
// 扣除金币
player.currency.copper -= price
// 添加物品
const result = addItemToInventory(player, item.id, 1)
if (result.success) {
game.addLog(`购买了 ${result.item.name}`, 'reward')
// 减少商店库存
const shop = getShopConfig(player.currentLocation)
if (shop) {
const shopItem = shop.items.find(i => i.itemId === item.id)
if (shopItem && shopItem.stock > 0) {
shopItem.stock--
}
}
}
selectedItem.value = null
}
function confirmSell() {
if (!selectedItem.value) return
const item = selectedItem.value
const sellCount = item.sellCount || 1
const price = getSellPrice(item) * sellCount
// 移除物品
const result = removeItemFromInventory(player, item.uniqueId || item.id, sellCount)
if (result.success) {
// 增加金币
player.currency.copper += price
const countText = sellCount > 1 ? `x${sellCount}` : ''
game.addLog(`出售了 ${item.name}${countText},获得 ${price} 铜币`, 'reward')
}
selectedItem.value = null
}
// 确认数量选择出售
function confirmQuantitySell() {
if (!quantitySelector.value) return
const { item, selectedCount } = quantitySelector.value
const price = getSellPrice(item) * selectedCount
// 移除指定数量的物品
const result = removeItemFromInventory(player, item.uniqueId || item.id, selectedCount)
if (result.success) {
// 增加金币
player.currency.copper += price
game.addLog(`出售了 ${item.name}x${selectedCount},获得 ${price} 铜币`, 'reward')
}
// 关闭弹窗
quantitySelector.value = null
}
// 取消数量选择
function cancelQuantitySell() {
quantitySelector.value = null
}
// 增加出售数量
function increaseSellCount() {
if (quantitySelector.value && quantitySelector.value.selectedCount < quantitySelector.value.maxCount) {
quantitySelector.value.selectedCount++
}
}
// 减少出售数量
function decreaseSellCount() {
if (quantitySelector.value && quantitySelector.value.selectedCount > 1) {
quantitySelector.value.selectedCount--
}
}
// 设置最大出售数量
function setMaxSellCount() {
if (quantitySelector.value) {
quantitySelector.value.selectedCount = quantitySelector.value.maxCount
}
}
function close() {
emit('close')
}
</script>
<style lang="scss" scoped>
.shop-drawer {
height: 100%;
display: flex;
flex-direction: column;
background-color: $bg-secondary;
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 24rpx;
border-bottom: 1rpx solid $border-color;
}
.header-left {
flex: 1;
}
.drawer-title {
display: block;
color: $text-primary;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 4rpx;
}
.drawer-owner {
display: block;
color: $text-secondary;
font-size: 24rpx;
}
.drawer-close {
color: $text-secondary;
font-size: 48rpx;
padding: 0 16rpx;
&:active {
color: $text-primary;
}
}
.currency-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
background-color: rgba($accent, 0.1);
border-bottom: 1rpx solid $border-color;
}
.currency-label {
color: $text-secondary;
font-size: 24rpx;
}
.currency-value {
color: $accent;
font-size: 28rpx;
font-weight: bold;
}
.bulk-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 24rpx;
background-color: rgba($bg-primary, 0.5);
border-bottom: 1rpx solid $border-color;
&__left {
display: flex;
gap: 24rpx;
}
&__count {
color: $text-primary;
font-size: 24rpx;
}
&__total {
color: $accent;
font-size: 24rpx;
font-weight: bold;
}
&__right {
display: flex;
gap: 24rpx;
}
&__link {
color: $accent;
font-size: 24rpx;
&:active {
opacity: 0.7;
}
}
}
.bulk-confirm-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 24rpx;
background-color: rgba($accent, 0.1);
border-top: 1rpx solid $border-color;
}
.bulk-confirm-text {
color: $text-primary;
font-size: 24rpx;
}
.shop-list {
flex: 1;
padding: 16rpx;
}
.shop-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: $bg-primary;
border-radius: 8rpx;
margin-bottom: 8rpx;
&:active {
background-color: $bg-tertiary;
}
&--disabled {
opacity: 0.5;
}
&--selected {
background-color: rgba($accent, 0.15);
border: 1rpx solid $accent;
}
&--bulk {
cursor: pointer;
}
&__checkbox {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid $border-color;
border-radius: 4rpx;
margin-right: 12rpx;
background-color: $bg-secondary;
}
&__main {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
min-width: 0;
}
&__icon {
font-size: 40rpx;
}
&__info {
flex: 1;
min-width: 0;
}
&__name {
display: block;
color: $text-primary;
font-size: 28rpx;
}
&__desc {
display: block;
color: $text-secondary;
font-size: 22rpx;
margin-top: 4rpx;
}
&__count {
display: block;
color: $text-muted;
font-size: 22rpx;
}
&__right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4rpx;
}
&__price {
color: $accent;
font-size: 26rpx;
font-weight: bold;
}
&__stock {
color: $text-muted;
font-size: 20rpx;
}
}
.checkbox-icon {
color: $accent;
font-size: 24rpx;
font-weight: bold;
}
.shop-empty {
padding: 80rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 28rpx;
}
}
.item-detail-popup {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.item-detail {
width: 80%;
max-width: 500rpx;
padding: 32rpx;
background-color: $bg-secondary;
border-radius: 16rpx;
&__name {
display: block;
color: $text-primary;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 16rpx;
}
&__desc {
display: block;
color: $text-secondary;
font-size: 24rpx;
line-height: 1.6;
margin-bottom: 24rpx;
}
&__actions {
display: flex;
gap: 12rpx;
justify-content: flex-end;
}
}
// 数量选择器样式
.quantity-selector {
&__item-name {
display: block;
color: $text-primary;
font-size: 28rpx;
margin-bottom: 8rpx;
}
&__available {
display: block;
color: $text-secondary;
font-size: 24rpx;
margin-bottom: 16rpx;
}
&__controls {
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
margin-bottom: 16rpx;
}
&__count {
min-width: 80rpx;
text-align: center;
color: $text-primary;
font-size: 32rpx;
font-weight: bold;
}
&__preview {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: $bg-tertiary;
border-radius: 8rpx;
margin-top: 16rpx;
}
&__price-label {
color: $text-secondary;
font-size: 24rpx;
}
&__price {
color: $accent;
font-size: 28rpx;
font-weight: bold;
}
}
</style>