Browse Source

增加一个图鉴,用来展示系统的物品信息

master
秦汉 2 weeks ago
parent
commit
4976ae2e68
  1. 73
      Build_God_Game/src/api/catalog.ts
  2. 1
      Build_God_Game/src/assets/images/catalog.svg
  3. 25
      Build_God_Game/src/components/GooeyNav/GooeyNav.vue
  4. 6
      Build_God_Game/src/router/index.ts
  5. 453
      Build_God_Game/src/views/CatalogView.vue
  6. 4
      Build_God_Game/src/views/GameView.vue

73
Build_God_Game/src/api/catalog.ts

@ -0,0 +1,73 @@
import http from './index'
export interface PagedResult<T> {
items: T[]
totalCount: number
pageNumber: number
pageSize: number
}
export interface EquipmentTemplateDto {
id: number
name: string
description: string
type: number
rarity: number
requirdLevelId: number
money: number
randomAttrCount: number
maxEnhanceLevel: number
icon?: string | null
}
export interface PillDto {
id: number
name: string
type: number
rarity: number
description: string
requirdLevelId: number
effectValue: number
duration: number
icon?: string | null
}
export interface LevelDto {
id: number
name: string
levelId: number
currentLevelMinExp: number
nextLevelId?: number | null
baseBreakthroughRate: number
failIncrement: number
description: string
requiredPillId?: number | null
requiredPillQuantity: number
}
export interface ScrapListDto {
id: number
name: string
description: string
story: string
level: number
levelName: string
levelColor: string
attackBonus: number
defenseBonus: number
hpBonus: number
magicBonus: number
isActive: boolean
icon?: string | null
}
export const catalogApi = {
getEquipmentTemplates: (pageSize = 500) =>
http.post('equipment/all', {
pageNumber: 1,
pageSize,
}) as Promise<PagedResult<EquipmentTemplateDto>>,
getPills: () => http.get('pill/all') as Promise<PillDto[]>,
getLevels: () => http.get('level/all') as Promise<LevelDto[]>,
getScraps: () => http.get('scrap/list') as Promise<ScrapListDto[]>,
}

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

25
Build_God_Game/src/components/GooeyNav/GooeyNav.vue

@ -1,5 +1,5 @@
<template>
<div>
<div class="gooey-nav-root">
<div class="relative" ref="containerRef">
<nav class="flex relative" :style="{ transform: 'translate3d(0,0,0.01px)' }">
<ul
@ -20,9 +20,9 @@
>
<a
:href="item.href || undefined"
class="gooey-nav-link"
@click="e => handleClick(e, index)"
@keydown="e => handleKeyDown(e, index)"
class="outline-none py-[0.6em] px-[1em] inline-block"
>
{{ item.label }}
</a>
@ -56,6 +56,11 @@ interface GooeyNavProps {
initialActiveIndex?: number;
}
const emit = defineEmits<{
/** 用户切换到某一索引时触发(与同一项重复点击不会触发) */
select: [index: number];
}>();
const props = withDefaults(defineProps<GooeyNavProps>(), {
animationTime: 600,
particleCount: 15,
@ -145,9 +150,14 @@ const updateEffectPosition = (element: HTMLElement) => {
};
const handleClick = (e: Event, index: number) => {
const item = props.items[index];
if (!item?.href) {
e.preventDefault();
}
const liEl = (e.currentTarget as HTMLElement).parentElement as HTMLElement;
if (activeIndex.value === index) return;
activeIndex.value = index;
emit('select', index);
updateEffectPosition(liEl);
if (filterRef.value) {
const particles = filterRef.value.querySelectorAll('.particle');
@ -211,6 +221,15 @@ onUnmounted(() => {
</script>
<style>
/* 使用组件内 CSS 控制留白:避免仅依赖 Tailwind 时未参与编译导致 padding 不生效 */
.gooey-nav-root .gooey-nav-link {
display: inline-block;
outline: none;
padding: 12px 26px;
line-height: 1.3;
box-sizing: border-box;
}
:root {
--linear-ease: linear(
0,
@ -379,7 +398,7 @@ li::after {
content: '';
position: absolute;
inset: 0;
border-radius: 8px;
border-radius: 9999px;
background: white;
opacity: 0;
transform: scale(0);

6
Build_God_Game/src/router/index.ts

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

453
Build_God_Game/src/views/CatalogView.vue

@ -0,0 +1,453 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import Particles from '@/components/Particles/Particles.vue'
import GooeyNav from '@/components/GooeyNav/GooeyNav.vue'
import {
catalogApi,
type EquipmentTemplateDto,
type PillDto,
type LevelDto,
type ScrapListDto,
} from '@/api/catalog'
import itemDefaultIcon from '@/assets/images/item-default.svg'
type CatalogTab = 'equipment' | 'pill' | 'scrap' | 'level'
const router = useRouter()
const activeTab = ref<CatalogTab>('equipment')
const loading = ref(false)
const loadError = ref('')
const equipmentList = ref<EquipmentTemplateDto[]>([])
const pills = ref<PillDto[]>([])
const scraps = ref<ScrapListDto[]>([])
const levels = ref<LevelDto[]>([])
const equipmentTypeLabels: Record<number, string> = {
1: '武器',
2: '防具',
3: '头盔',
4: '项链',
5: '戒指',
6: '鞋子',
}
const equipRarityLabels: Record<number, string> = {
1: '普通',
2: '稀有',
3: '史诗',
4: '传说',
}
const pillTypeLabels: Record<number, string> = {
1: '增益',
2: '疗伤',
3: '突破',
4: '修炼',
}
const pillRarityLabels: Record<number, string> = {
1: '下品',
2: '中品',
3: '上品',
4: '极品',
}
const tabs: { key: CatalogTab; label: string }[] = [
{ key: 'equipment', label: '装备' },
{ key: 'pill', label: '丹药' },
{ key: 'scrap', label: '垃圾' },
{ key: 'level', label: '境界' },
]
const gooeyNavItems = computed(() =>
tabs.map((t) => ({ label: t.label, href: null as string | null }))
)
const onGooeyNavSelect = (index: number) => {
const t = tabs[index]
if (t) activeTab.value = t.key
}
const resolveIcon = (folder: 'equipment' | 'pill' | 'scrap', icon?: string | null) => {
if (!icon) return itemDefaultIcon
try {
return new URL(`../assets/icons/${folder}/${icon}`, import.meta.url).href
} catch {
return itemDefaultIcon
}
}
const formatExp = (n: number) => Math.floor(n).toLocaleString('zh-CN')
const formatPillDuration = (seconds: number) => {
if (!seconds || seconds <= 0) return '永久'
if (seconds >= 86400) return `${Math.round(seconds / 86400)}`
if (seconds >= 3600) return `${Math.round(seconds / 3600)} 小时`
if (seconds >= 60) return `${Math.round(seconds / 60)} 分钟`
return `${seconds}`
}
const loadAll = async () => {
loading.value = true
loadError.value = ''
try {
const [equipPage, pillRows, scrapRows, levelRows] = await Promise.all([
catalogApi.getEquipmentTemplates(),
catalogApi.getPills(),
catalogApi.getScraps(),
catalogApi.getLevels(),
])
equipmentList.value = equipPage.items ?? []
pills.value = pillRows ?? []
scraps.value = scrapRows ?? []
levels.value = levelRows ?? []
} catch (e: unknown) {
loadError.value = e instanceof Error ? e.message : String(e)
} finally {
loading.value = false
}
}
const goBack = () => {
router.push('/game')
}
onMounted(() => {
loadAll()
})
</script>
<template>
<div class="catalog-page">
<Particles :particle-count="40" :particle-colors="['#ffffff', '#888888']" class="particles-bg" />
<div class="page-container">
<div class="page-header">
<span class="back-btn" @click="goBack"> 返回</span>
<span class="title">图鉴</span>
<span class="header-placeholder"></span>
</div>
<p class="hint">以下为系统中的物品与境界配置阅览与实际背包无关</p>
<div class="catalog-nav-wrap">
<GooeyNav :items="gooeyNavItems" :initial-active-index="0" @select="onGooeyNavSelect" :animation-time="600"
:particle-count="15" :particle-distances="[90, 10]" :particle-r="100" :time-variance="300"
:colors="[1, 2, 3, 1, 2, 3, 1, 4]" />
</div>
<div v-if="loading" class="state-msg">加载中...</div>
<div v-else-if="loadError" class="state-msg error">{{ loadError }}</div>
<template v-else>
<!-- 装备 -->
<div v-show="activeTab === 'equipment'" class="panel">
<div class="panel-title">装备 · {{ equipmentList.length }} </div>
<div v-if="equipmentList.length === 0" class="empty">暂无装备模板数据</div>
<div v-else class="card-list">
<div v-for="item in equipmentList" :key="item.id" class="catalog-card">
<img class="thumb" :src="resolveIcon('equipment', item.icon)" alt="" />
<div class="body">
<div class="name">{{ item.name }}</div>
<div class="meta">
{{ equipmentTypeLabels[item.type] ?? `类型${item.type}` }}
· {{ equipRarityLabels[item.rarity] ?? `稀有度${item.rarity}` }}
· 需求境界 {{ item.requirdLevelId }}
</div>
<div class="desc">{{ item.description || '—' }}</div>
<div class="extra">
灵石 {{ item.money }} · 随机属性 {{ item.randomAttrCount }} · 最高强化 +{{ item.maxEnhanceLevel }}
</div>
</div>
</div>
</div>
</div>
<!-- 丹药 -->
<div v-show="activeTab === 'pill'" class="panel">
<div class="panel-title">丹药 · {{ pills.length }} </div>
<div v-if="pills.length === 0" class="empty">暂无丹药数据</div>
<div v-else class="card-list">
<div v-for="item in pills" :key="item.id" class="catalog-card">
<img class="thumb" :src="resolveIcon('pill', item.icon)" alt="" />
<div class="body">
<div class="name">{{ item.name }}</div>
<div class="meta">
{{ pillTypeLabels[item.type] ?? `类型${item.type}` }}
· {{ pillRarityLabels[item.rarity] ?? `品级${item.rarity}` }}
· 需求境界 {{ item.requirdLevelId }}
</div>
<div class="desc">{{ item.description || '—' }}</div>
<div class="extra">
效果数值 {{ item.effectValue }} · 持续 {{ formatPillDuration(Number(item.duration)) }}
</div>
</div>
</div>
</div>
</div>
<!-- 垃圾 -->
<div v-show="activeTab === 'scrap'" class="panel">
<div class="panel-title">垃圾 · {{ scraps.length }} 当前可产出</div>
<div v-if="scraps.length === 0" class="empty">暂无垃圾数据</div>
<div v-else class="card-list">
<div v-for="item in scraps" :key="item.id" class="catalog-card">
<img class="thumb" :src="resolveIcon('scrap', item.icon)" alt="" />
<div class="body">
<div class="name" :style="{ color: item.levelColor || '#fff' }">{{ item.name }}</div>
<div class="meta">
{{ item.levelName || `品级${item.level}` }}
<template v-if="!item.isActive"> · 未启用</template>
</div>
<div class="desc">{{ item.description || '—' }}</div>
<div class="extra stats-line">
<span v-if="item.attackBonus">+{{ item.attackBonus }}</span>
<span v-if="item.defenseBonus">+{{ item.defenseBonus }}</span>
<span v-if="item.hpBonus">+{{ item.hpBonus }}</span>
<span v-if="item.magicBonus">+{{ item.magicBonus }}</span>
<span v-if="!item.attackBonus && !item.defenseBonus && !item.hpBonus && !item.magicBonus">无数值加成</span>
</div>
<details v-if="item.story" class="story">
<summary>轶事</summary>
<p>{{ item.story }}</p>
</details>
</div>
</div>
</div>
</div>
<!-- 境界 -->
<div v-show="activeTab === 'level'" class="panel">
<div class="panel-title">境界 · {{ levels.length }} </div>
<div v-if="levels.length === 0" class="empty">暂无境界数据</div>
<div v-else class="card-list">
<div v-for="item in levels" :key="item.id" class="catalog-card level-card">
<div class="body">
<div class="name">{{ item.name }}</div>
<div class="meta">
层级 {{ item.levelId }}
<template v-if="item.nextLevelId"> · 下一境界 ID {{ item.nextLevelId }}</template>
</div>
<div class="desc">{{ item.description || '—' }}</div>
<div class="extra">
本级经验门槛 {{ formatExp(Number(item.currentLevelMinExp)) }}
· 基础突破 {{ Number(item.baseBreakthroughRate).toFixed(1) }}%
· 失败递增 +{{ Number(item.failIncrement).toFixed(1) }}%
</div>
<div v-if="item.requiredPillId && item.requiredPillQuantity > 0" class="extra pill-line">
突破消耗丹药 ID {{ item.requiredPillId }} × {{ item.requiredPillQuantity }}
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.catalog-page {
min-height: 100vh;
background: #000;
padding: 16px;
position: relative;
}
.particles-bg {
position: fixed !important;
inset: 0;
width: 100% !important;
height: 100% !important;
}
.page-container {
max-width: 520px;
margin: 0 auto;
position: relative;
z-index: 10;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.back-btn {
color: #777;
font-size: 0.9rem;
cursor: pointer;
padding: 8px;
}
.back-btn:hover {
color: #fff;
}
.title {
color: #fff;
font-size: 1.1rem;
font-weight: 600;
}
.header-placeholder {
width: 56px;
}
.hint {
color: #666;
font-size: 0.78rem;
line-height: 1.45;
margin-bottom: 14px;
}
.catalog-nav-wrap {
margin-bottom: 16px;
display: flex;
justify-content: center;
width: 100%;
max-width: 100%;
overflow: visible;
padding: 6px 0 14px;
}
/* .catalog-nav-wrap :deep(ul) {
flex-wrap: wrap;
justify-content: center;
row-gap: 0.35rem;
column-gap: 0.45rem !important;
padding-left: 0.25rem !important;
padding-right: 0.25rem !important;
max-width: 100%;
box-sizing: border-box;
text-shadow: none !important;
}
.catalog-nav-wrap :deep(li a),
.catalog-nav-wrap :deep(li.active a) {
text-shadow: none !important;
}
.catalog-nav-wrap :deep(.effect.text) {
text-shadow: none !important;
}
.catalog-nav-wrap :deep(a) {
font-size: 0.82rem;
padding: 0.55em 1.25em !important;
} */
.state-msg {
text-align: center;
color: #888;
padding: 32px;
}
.state-msg.error {
color: #f87171;
}
.panel-title {
color: #888;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.06em;
margin-bottom: 10px;
}
.empty {
color: #555;
text-align: center;
padding: 24px;
font-size: 0.9rem;
}
.card-list {
display: flex;
flex-direction: column;
gap: 10px;
padding-bottom: 28px;
}
.catalog-card {
display: flex;
gap: 12px;
padding: 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
}
.level-card {
padding-left: 14px;
}
.thumb {
width: 52px;
height: 52px;
flex-shrink: 0;
border-radius: 10px;
object-fit: contain;
background: rgba(0, 0, 0, 0.35);
}
.body {
min-width: 0;
flex: 1;
}
.name {
color: #fff;
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 4px;
}
.meta {
color: #777;
font-size: 0.72rem;
margin-bottom: 6px;
line-height: 1.35;
}
.desc {
color: #aaa;
font-size: 0.8rem;
line-height: 1.5;
margin-bottom: 6px;
}
.extra {
color: #666;
font-size: 0.72rem;
line-height: 1.4;
}
.stats-line span {
margin-right: 10px;
}
.pill-line {
margin-top: 4px;
color: #a78bfa;
}
.story {
margin-top: 8px;
font-size: 0.72rem;
color: #888;
}
.story summary {
cursor: pointer;
color: #999;
}
.story p {
margin: 6px 0 0;
line-height: 1.45;
color: #777;
}
</style>

4
Build_God_Game/src/views/GameView.vue

@ -16,6 +16,7 @@ import characterIco from '@/assets/images/character.svg'
import bagIcon from '@/assets/images/bag.svg'
import monsterIcon from '@/assets/images/monster.svg'
import shopIcon from '@/assets/images/shop.svg'
import catalogIcon from '@/assets/images/catalog.svg'
const authStore = useAuthStore()
const characterStore = useCharacterStore()
@ -49,6 +50,7 @@ const menuItems = computed(() => [
{ label: '捡垃圾', icon: scrapIcon, useImage: true },
{ label: '商店', icon: shopIcon, useImage: true },
{ label: '挑战', icon: monsterIcon, useImage: true },
{ label: '图鉴', icon: catalogIcon, useImage: true }
])
const formatNumber = (num: number) => {
@ -84,6 +86,8 @@ const navigateTo = (item: { label: string }) => {
router.push('/shop')
} else if (item.label === '角色') {
openCharacterDetail()
} else if (item.label === '图鉴') {
router.push('/catalog')
}
}

Loading…
Cancel
Save