You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
8.8 KiB
394 lines
8.8 KiB
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useCharacterStore } from '@/stores/character'
|
|
import { getShop, buyItem, type Shop, type ShopItemDisplay } from '@/api/shop'
|
|
import { ElMessage } from 'element-plus'
|
|
import StarBorder from '@/components/StarBorder/StarBorder.vue'
|
|
import Particles from '@/components/Particles/Particles.vue'
|
|
|
|
const router = useRouter()
|
|
const characterStore = useCharacterStore()
|
|
|
|
const shop = ref<Shop | null>(null)
|
|
const loading = ref(true)
|
|
const buying = ref<number | null>(null)
|
|
const error = ref('')
|
|
|
|
const currentMoney = computed(() => characterStore.currentCharacter?.money ?? 0)
|
|
const characterId = computed(() => characterStore.currentCharacter?.id ?? 0)
|
|
|
|
const itemTypeMap: Record<number, string> = {
|
|
1: '装备',
|
|
2: '丹药',
|
|
3: '垃圾'
|
|
}
|
|
|
|
const rarityMap: Record<number, string> = {
|
|
1: '普通',
|
|
2: '稀有',
|
|
3: '史诗',
|
|
4: '传说'
|
|
}
|
|
|
|
const rarityColorMap: Record<number, string> = {
|
|
1: '#9ca3af',
|
|
2: '#22c55e',
|
|
3: '#3b82f6',
|
|
4: '#f59e0b'
|
|
}
|
|
|
|
const getItemIcon = (item: ShopItemDisplay) => {
|
|
if (item.icon) {
|
|
const typeStr = item.itemType === 1 ? 'equipment' : item.itemType === 2 ? 'pill' : 'scrap'
|
|
return new URL(`../assets/icons/${typeStr}/${item.icon}`, import.meta.url).href
|
|
}
|
|
return null
|
|
}
|
|
|
|
const getItemEmoji = (itemType: number) => {
|
|
if (itemType === 1) return '⚔️'
|
|
if (itemType === 2) return '💊'
|
|
return '📦'
|
|
}
|
|
|
|
const canBuy = (item: ShopItemDisplay) => {
|
|
if (item.remainingStock <= 0) return false
|
|
if (item.dailyLimit > 0 && item.purchasedToday >= item.dailyLimit) return false
|
|
if (currentMoney.value < item.price) return false
|
|
return true
|
|
}
|
|
|
|
const getBuyButtonText = (item: ShopItemDisplay) => {
|
|
if (item.remainingStock <= 0) return '售罄'
|
|
if (item.dailyLimit > 0 && item.purchasedToday >= item.dailyLimit) return '已达上限'
|
|
if (currentMoney.value < item.price) return '灵石不足'
|
|
return '购买'
|
|
}
|
|
|
|
const formatRefreshTime = (lastRefreshTime: string) => {
|
|
const date = new Date(lastRefreshTime)
|
|
return date.toLocaleString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
}
|
|
|
|
const formatNumber = (num: number) => {
|
|
return Math.floor(num).toLocaleString('zh-CN')
|
|
}
|
|
|
|
const fetchShop = async () => {
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
shop.value = await getShop()
|
|
} catch (err: any) {
|
|
error.value = err.message || '加载商店失败'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleBuy = async (item: ShopItemDisplay) => {
|
|
if (!canBuy(item)) return
|
|
|
|
buying.value = item.shopItemId
|
|
try {
|
|
await buyItem(item.shopItemId)
|
|
ElMessage.success(`购买成功!${item.itemName}`)
|
|
await fetchShop()
|
|
// fetchCharacters 只更新列表,不更新 currentCharacter;灵石显示依赖后者
|
|
await characterStore.refreshCurrentCharacter()
|
|
} catch (err: any) {
|
|
ElMessage.error(err.message || '购买失败')
|
|
} finally {
|
|
buying.value = null
|
|
}
|
|
}
|
|
|
|
const goBack = () => {
|
|
router.push('/game')
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await characterStore.refreshCurrentCharacter()
|
|
await fetchShop()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="shop-page">
|
|
<Particles />
|
|
<StarBorder />
|
|
|
|
<div class="page-container">
|
|
<div class="page-header">
|
|
<span class="back-btn" @click="goBack">← 返回</span>
|
|
<span class="title">商店</span>
|
|
<span class="placeholder"></span>
|
|
</div>
|
|
|
|
<div v-if="shop" class="shop-info">
|
|
<span class="refresh-time">商品刷新时间: {{ formatRefreshTime(shop.lastRefreshTime) }}</span>
|
|
</div>
|
|
|
|
<div class="money-display">
|
|
💰 {{ formatNumber(currentMoney) }} 灵石
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">
|
|
加载中...
|
|
</div>
|
|
|
|
<div v-else-if="error" class="error">
|
|
{{ error }}
|
|
<button class="retry-btn" @click="fetchShop">重试</button>
|
|
</div>
|
|
|
|
<div v-else-if="!shop || shop.items.length === 0" class="empty">
|
|
商店暂无可购商品,请联系管理员上架商品
|
|
</div>
|
|
|
|
<div v-else class="shop-grid">
|
|
<div
|
|
v-for="item in shop.items"
|
|
:key="item.shopItemId"
|
|
class="shop-item"
|
|
:class="{ 'cannot-buy': !canBuy(item) }"
|
|
>
|
|
<div class="item-icon-wrapper">
|
|
<img
|
|
v-if="getItemIcon(item)"
|
|
:src="getItemIcon(item)"
|
|
class="item-icon-img"
|
|
:class="'rarity-' + (item.itemRarity || 1)"
|
|
/>
|
|
<div v-else class="item-icon">{{ getItemEmoji(item.itemType) }}</div>
|
|
<div v-if="item.itemRarity" class="rarity-badge" :style="{ color: rarityColorMap[item.itemRarity] }">
|
|
{{ rarityMap[item.itemRarity] }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="item-name">{{ item.itemName }}</div>
|
|
|
|
<div class="item-price">
|
|
💰 {{ formatNumber(item.price) }}
|
|
</div>
|
|
|
|
<div class="item-limit" v-if="item.dailyLimit > 0">
|
|
今日已购: {{ item.purchasedToday }} / {{ item.dailyLimit }}
|
|
</div>
|
|
|
|
<button
|
|
class="buy-btn"
|
|
:disabled="!canBuy(item) || buying === item.shopItemId"
|
|
@click="handleBuy(item)"
|
|
>
|
|
{{ buying === item.shopItemId ? '购买中...' : getBuyButtonText(item) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="css">
|
|
.shop-page {
|
|
min-height: 100vh;
|
|
background: #000000;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 0;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.page-container {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
position: relative;
|
|
z-index: 10;
|
|
}
|
|
|
|
.back-btn {
|
|
color: #666666;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
padding: 8px;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.title {
|
|
color: #ffffff;
|
|
font-size: 1.1rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.placeholder {
|
|
width: 60px;
|
|
}
|
|
|
|
.money-display {
|
|
padding: 8px 16px;
|
|
background: rgba(255, 215, 0, 0.1);
|
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
|
border-radius: 8px;
|
|
color: #ffd700;
|
|
font-size: 1rem;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.shop-info {
|
|
margin-bottom: 20px;
|
|
color: #888888;
|
|
font-size: 0.9rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.refresh-time {
|
|
padding: 4px 12px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.loading,
|
|
.error,
|
|
.empty {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #888888;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.error {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.retry-btn {
|
|
display: block;
|
|
margin: 16px auto 0;
|
|
padding: 8px 24px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 8px;
|
|
color: #cccccc;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.shop-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.shop-item {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.shop-item:hover {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.shop-item.cannot-buy {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.item-icon-wrapper {
|
|
position: relative;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.item-icon-img {
|
|
width: 48px;
|
|
height: 48px;
|
|
object-fit: contain;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.item-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.rarity-1 { filter: grayscale(0.3); }
|
|
.rarity-2 { filter: hue-rotate(80deg) saturate(1.5); }
|
|
.rarity-3 { filter: hue-rotate(200deg) saturate(2); }
|
|
.rarity-4 { filter: hue-rotate(-30deg) saturate(2) brightness(1.2); }
|
|
|
|
.rarity-badge {
|
|
position: absolute;
|
|
bottom: -6px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
font-size: 0.65rem;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.item-name {
|
|
color: #ffffff;
|
|
font-size: 0.85rem;
|
|
margin-bottom: 6px;
|
|
min-height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.item-price {
|
|
color: #ffd700;
|
|
font-size: 0.9rem;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.item-limit {
|
|
color: #888888;
|
|
font-size: 0.75rem;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.buy-btn {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: #ffffff;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.buy-btn:hover:not(:disabled) {
|
|
transform: scale(1.02);
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.buy-btn:disabled {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
</style>
|