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.
454 lines
12 KiB
454 lines
12 KiB
|
2 weeks ago
|
<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>
|