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>
634 lines
14 KiB
Vue
634 lines
14 KiB
Vue
<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>
|