Browse Source

游戏里面的商店系统

master
秦汉 3 weeks ago
parent
commit
f0616d2c67
  1. 1
      Build_God_Api/Build_God_Api/DB/BagItem.cs
  2. 3
      Build_God_Api/Build_God_Api/Program.cs
  3. 28
      Build_God_Game/src/api/shop.ts
  4. 1
      Build_God_Game/src/assets/images/shop.svg
  5. 6
      Build_God_Game/src/router/index.ts
  6. 4
      Build_God_Game/src/views/GameView.vue
  7. 387
      Build_God_Game/src/views/ShopView.vue

1
Build_God_Api/Build_God_Api/DB/BagItem.cs

@ -31,6 +31,7 @@ namespace Build_God_Api.DB
/// <summary> /// <summary>
/// 是否绑定(绑定后不可交易/拍卖) /// 是否绑定(绑定后不可交易/拍卖)
/// </summary> /// </summary>
[SugarColumn(DefaultValue = "false")]
public bool IsBound { get; set; } public bool IsBound { get; set; }
} }

3
Build_God_Api/Build_God_Api/Program.cs

@ -98,6 +98,9 @@ namespace Build_God_Api
sqlSugarClient.CodeFirst.InitTables(typeof(Scrap)); sqlSugarClient.CodeFirst.InitTables(typeof(Scrap));
sqlSugarClient.CodeFirst.InitTables(typeof(Monster)); sqlSugarClient.CodeFirst.InitTables(typeof(Monster));
sqlSugarClient.CodeFirst.InitTables(typeof(MonsterReward)); sqlSugarClient.CodeFirst.InitTables(typeof(MonsterReward));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterShop));
sqlSugarClient.CodeFirst.InitTables(typeof(CharacterShopPurchaseLog));
sqlSugarClient.CodeFirst.InitTables(typeof(ShopItem));
return sqlSugarClient; return sqlSugarClient;
}); });

28
Build_God_Game/src/api/shop.ts

@ -0,0 +1,28 @@
import http from './index'
export interface ShopItemDisplay {
shopItemId: number
itemType: number
itemId: number
itemName: string | null
price: number
dailyLimit: number
purchasedToday: number
remainingStock: number
icon: string | null
itemRarity: number | null
}
export interface Shop {
characterId: number
lastRefreshTime: string
items: ShopItemDisplay[]
}
export const getShop = (): Promise<Shop> => {
return http.get('shop')
}
export const buyItem = (shopItemId: number): Promise<boolean> => {
return http.post('shop/buy', { shopItemId })
}

1
Build_God_Game/src/assets/images/shop.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

6
Build_God_Game/src/router/index.ts

@ -75,6 +75,12 @@ const router = createRouter({
component: () => import('@/views/BattleView.vue'), component: () => import('@/views/BattleView.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{
path: '/shop',
name: 'shop',
component: () => import('@/views/ShopView.vue'),
meta: { requiresAuth: true }
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound', name: 'NotFound',

4
Build_God_Game/src/views/GameView.vue

@ -15,6 +15,7 @@ import scrapIcon from '@/assets/images/scrap.svg'
import characterIco from '@/assets/images/character.svg' import characterIco from '@/assets/images/character.svg'
import bagIcon from '@/assets/images/bag.svg' import bagIcon from '@/assets/images/bag.svg'
import monsterIcon from '@/assets/images/monster.svg' import monsterIcon from '@/assets/images/monster.svg'
import shopIcon from '@/assets/images/shop.svg'
const authStore = useAuthStore() const authStore = useAuthStore()
const characterStore = useCharacterStore() const characterStore = useCharacterStore()
@ -46,6 +47,7 @@ const menuItems = computed(() => [
{ label: isTraining.value ? '打坐中' : '打坐', icon: trainingIcon, useImage: true, isTraining: isTraining.value }, { label: isTraining.value ? '打坐中' : '打坐', icon: trainingIcon, useImage: true, isTraining: isTraining.value },
{ label: '背包', icon: bagIcon, useImage: true }, { label: '背包', icon: bagIcon, useImage: true },
{ label: '捡垃圾', icon: scrapIcon, useImage: true }, { label: '捡垃圾', icon: scrapIcon, useImage: true },
{ label: '商店', icon: shopIcon, useImage: true },
{ label: '挑战', icon: monsterIcon, useImage: true }, { label: '挑战', icon: monsterIcon, useImage: true },
]) ])
@ -78,6 +80,8 @@ const navigateTo = (item: { label: string }) => {
router.push('/scrap') router.push('/scrap')
} else if (item.label === '挑战') { } else if (item.label === '挑战') {
router.push('/monster-list') router.push('/monster-list')
} else if (item.label === '商店') {
router.push('/shop')
} else if (item.label === '角色') { } else if (item.label === '角色') {
openCharacterDetail() openCharacterDetail()
} }

387
Build_God_Game/src/views/ShopView.vue

@ -0,0 +1,387 @@
<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'
import { ArrowLeft } from '@element-plus/icons-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()
await characterStore.fetchCharacters()
} catch (err: any) {
ElMessage.error(err.message || '购买失败')
} finally {
buying.value = null
}
}
const goBack = () => {
router.push('/game')
}
onMounted(async () => {
await characterStore.fetchCharacters()
await fetchShop()
})
</script>
<template>
<div class="shop-container">
<Particles />
<StarBorder />
<div class="header">
<button class="back-btn" @click="goBack">
<span class="back-icon"></span>
<span>返回</span>
</button>
<h1 class="title">商店</h1>
<div class="money-display">
💰 {{ formatNumber(currentMoney) }} 灵石
</div>
</div>
<div v-if="shop" class="shop-info">
<span class="refresh-time">商品刷新时间: {{ formatRefreshTime(shop.lastRefreshTime) }}</span>
</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>
</template>
<style scoped lang="css">
.shop-container {
min-height: 100vh;
padding: 20px;
position: relative;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.back-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #cccccc;
cursor: pointer;
transition: all 0.2s ease;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.back-icon {
font-size: 1.2rem;
}
.title {
font-size: 1.5rem;
color: #ffffff;
margin: 0;
}
.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;
}
.shop-info {
margin-bottom: 20px;
color: #888888;
font-size: 0.9rem;
}
.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(160px, 1fr));
gap: 16px;
}
.shop-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
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: 12px;
}
.item-icon-img {
width: 64px;
height: 64px;
object-fit: contain;
border-radius: 8px;
}
.item-icon {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
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: -8px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
font-weight: bold;
white-space: nowrap;
}
.item-name {
color: #ffffff;
font-size: 0.95rem;
margin-bottom: 8px;
min-height: 40px;
display: flex;
align-items: center;
}
.item-price {
color: #ffd700;
font-size: 1rem;
margin-bottom: 8px;
}
.item-limit {
color: #888888;
font-size: 0.8rem;
margin-bottom: 12px;
}
.buy-btn {
width: 100%;
padding: 10px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: #ffffff;
font-size: 0.95rem;
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>
Loading…
Cancel
Save