Files

785 lines
19 KiB
Vue
Raw Permalink Normal View History

<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>