Files
text-adventure-game/components/drawers/ShopDrawer.vue
Claude cb412544e9 Initial commit: Text Adventure Game
Features:
- Combat system with AP/EP hit calculation and three-layer defense
- Auto-combat/farming mode
- Item system with stacking support
- Skill system with levels, milestones, and parent skill sync
- Shop system with dynamic pricing
- Inventory management with bulk selling
- Event system
- Game loop with offline earnings
- Save/Load system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:13:51 +08:00

634 lines
14 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>
</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 { 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 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 => {
const itemConfig = { ...shopItem }
itemConfig.id = shopItem.itemId
return itemConfig
})
})
// 玩家可出售物品
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 {
// 普通模式:显示详情
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 price = getSellPrice(item)
// 移除物品
const result = removeItemFromInventory(player, item.uniqueId || item.id)
if (result.success) {
// 增加金币
player.currency.copper += price
game.addLog(`出售了 ${item.name},获得 ${price} 铜币`, 'reward')
}
selectedItem.value = null
}
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;
}
}
</style>