mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 16:05:12 +08:00
refactor: 优化主界面布局与通知系统
重构App.vue,首页独立无侧边栏,其他页面采用统一侧边栏布局。新增右下角固定通知区,集成返回顶部、队列通知、外交通知和敌方警报。移除新手引导组件,替换为弱引导提示系统。支持星球重命名弹窗。优化NPC成长与行为定时器逻辑,提升性能和可维护性。删除issue模板及相关文档描述。
This commit is contained in:
@@ -1,111 +1,183 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('diplomacy.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">{{ t('diplomacy.description') }}</p>
|
||||
</div>
|
||||
<!-- NPC诊断按钮 -->
|
||||
<Button @click="showNPCDiagnostic" variant="outline" size="sm">
|
||||
<Search class="mr-2 h-4 w-4" />
|
||||
NPC状态诊断
|
||||
</Button>
|
||||
<!-- 视图切换和诊断按钮 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<!-- 视图模式切换 -->
|
||||
<div class="flex items-center border rounded-md">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 rounded-r-none"
|
||||
:class="{ 'bg-accent': viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
:title="t('diplomacy.viewMode.list')"
|
||||
>
|
||||
<List class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 rounded-l-none border-l"
|
||||
:class="{ 'bg-accent': viewMode === 'card' }"
|
||||
@click="viewMode = 'card'"
|
||||
:title="t('diplomacy.viewMode.card')"
|
||||
>
|
||||
<LayoutGrid class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- NPC诊断按钮 -->
|
||||
<Button @click="showNPCDiagnostic" variant="outline" size="sm">
|
||||
<Activity class="h-4 w-4 sm:mr-2" />
|
||||
<span class="hidden sm:inline">{{ t('diplomacy.diagnostic.button') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NPC诊断对话框 -->
|
||||
<Dialog v-model:open="npcDiagnosticOpen">
|
||||
<ScrollableDialogContent container-class="max-w-4xl">
|
||||
<template #header>
|
||||
<DialogTitle>NPC状态诊断报告</DialogTitle>
|
||||
<DialogTitle>{{ t('diplomacy.diagnostic.title') }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div class="text-sm mt-2">
|
||||
玩家积分: {{ gameStore.player.points || 0 }} | 侦查间隔: {{ Math.floor(behaviorConfig.spyInterval / 60) }}分钟 | 攻击间隔:
|
||||
{{ Math.floor(behaviorConfig.attackInterval / 60) }}分钟 | 攻击概率:
|
||||
{{ (behaviorConfig.attackProbability * 100).toFixed(0) }}%
|
||||
{{
|
||||
t('diplomacy.diagnostic.description', {
|
||||
points: gameStore.player.points || 0,
|
||||
spyInterval: Math.floor(behaviorConfig.spyInterval / 60),
|
||||
attackInterval: Math.floor(behaviorConfig.attackInterval / 60),
|
||||
attackProb: (behaviorConfig.attackProbability * 100).toFixed(0)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</template>
|
||||
|
||||
<div v-if="npcDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">暂无NPC数据</div>
|
||||
<!-- 诊断对话框搜索框 -->
|
||||
<div class="relative mb-4">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="diagnosticSearchQuery" type="text" :placeholder="t('diplomacy.searchPlaceholder')" class="pl-10" />
|
||||
</div>
|
||||
|
||||
<div v-if="filteredDiagnostics.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
{{ t('diplomacy.diagnostic.noData') }}
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="diagnostic in npcDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||
<div v-for="diagnostic in paginatedDiagnostics" :key="diagnostic.npcId" class="border rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-bold text-lg">{{ diagnostic.npcName }}</h3>
|
||||
<Badge
|
||||
:variant="
|
||||
diagnostic.relationStatus === '友好' ? 'default' : diagnostic.relationStatus === '敌对' ? 'destructive' : 'secondary'
|
||||
"
|
||||
>
|
||||
{{ diagnostic.relationStatus }}
|
||||
<Badge :variant="getRelationBadgeVariant(diagnostic.relationStatusKey)">
|
||||
{{ getLocalizedRelationStatus(diagnostic.relationStatusKey) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">难度:</span>
|
||||
<span class="font-medium">{{ diagnostic.difficulty }}</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.difficulty') }}:</span>
|
||||
<span class="font-medium">{{ t(`diplomacy.diagnostic.difficultyLevels.${diagnostic.difficulty}`) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">好感度:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.reputation') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.reputation }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">侦查探测器:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.spyProbes') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.spyProbes }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">舰队战力:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.fleetPower') }}:</span>
|
||||
<span class="font-medium">{{ diagnostic.totalFleetPower }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">可以侦查:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.canSpy') }}:</span>
|
||||
<span :class="diagnostic.canSpy ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||
{{ diagnostic.canSpy ? '是' : '否' }}
|
||||
{{ diagnostic.canSpy ? t('diplomacy.diagnostic.yes') : t('diplomacy.diagnostic.no') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">可以攻击:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.canAttack') }}:</span>
|
||||
<span :class="diagnostic.canAttack ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
|
||||
{{ diagnostic.canAttack ? '是' : '否' }}
|
||||
{{ diagnostic.canAttack ? t('diplomacy.diagnostic.yes') : t('diplomacy.diagnostic.no') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">攻击概率:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.attackProbability') }}:</span>
|
||||
<span class="font-medium">{{ (diagnostic.attackProbability * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">下次侦查:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.nextSpy') }}:</span>
|
||||
<span class="font-medium">
|
||||
<template v-if="diagnostic.nextSpyIn > 0">
|
||||
{{ Math.floor(diagnostic.nextSpyIn / 60) }}分{{ diagnostic.nextSpyIn % 60 }}秒
|
||||
{{
|
||||
t('diplomacy.diagnostic.timeFormat', { min: Math.floor(diagnostic.nextSpyIn / 60), sec: diagnostic.nextSpyIn % 60 })
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-green-600">随时</span>
|
||||
<span class="text-green-600">{{ t('diplomacy.diagnostic.anytime') }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<span class="text-muted-foreground">下次攻击:</span>
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.diagnostic.nextAttack') }}:</span>
|
||||
<span class="font-medium">
|
||||
<template v-if="diagnostic.nextAttackIn > 0">
|
||||
{{ Math.floor(diagnostic.nextAttackIn / 60) }}分{{ diagnostic.nextAttackIn % 60 }}秒
|
||||
{{
|
||||
t('diplomacy.diagnostic.timeFormat', {
|
||||
min: Math.floor(diagnostic.nextAttackIn / 60),
|
||||
sec: diagnostic.nextAttackIn % 60
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-green-600">随时</span>
|
||||
<span class="text-green-600">{{ t('diplomacy.diagnostic.anytime') }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="diagnostic.reasons.length > 0" class="mt-3 p-3 bg-muted rounded text-xs">
|
||||
<div class="font-semibold mb-2">状态说明:</div>
|
||||
<div class="font-semibold mb-2">{{ t('diplomacy.diagnostic.statusExplanation') }}:</div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ reason }}</li>
|
||||
<li v-for="(reason, idx) in diagnostic.reasons" :key="idx">{{ translateReason(reason) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控制 -->
|
||||
<Pagination
|
||||
v-if="diagnosticTotalPages > 1"
|
||||
v-model:page="diagnosticPage"
|
||||
:total="filteredDiagnostics.length"
|
||||
:items-per-page="DIAGNOSTIC_ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in diagnosticPageNumbers" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === diagnosticPage">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</ScrollableDialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="searchQuery" type="text" :placeholder="t('diplomacy.searchPlaceholder')" class="pl-10" />
|
||||
</div>
|
||||
|
||||
<!-- 关系状态过滤标签 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-4">
|
||||
@@ -149,169 +221,149 @@
|
||||
|
||||
<!-- 全部NPC -->
|
||||
<TabsContent value="all" class="space-y-4 mt-6">
|
||||
<div v-if="allNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="allNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Users class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedAllNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedAllNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesAll > 1"
|
||||
v-model:page="currentPage.all"
|
||||
:total="allNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersAll" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.all">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 友好NPC -->
|
||||
<TabsContent value="friendly" class="space-y-4 mt-6">
|
||||
<div v-if="friendlyNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noFriendlyNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="friendlyNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Heart class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noFriendlyNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedFriendlyNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedFriendlyNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesFriendly > 1"
|
||||
v-model:page="currentPage.friendly"
|
||||
:total="friendlyNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersFriendly" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.friendly">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 中立NPC -->
|
||||
<TabsContent value="neutral" class="space-y-4 mt-6">
|
||||
<div v-if="neutralNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noNeutralNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="neutralNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Minus class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noNeutralNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedNeutralNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedNeutralNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesNeutral > 1"
|
||||
v-model:page="currentPage.neutral"
|
||||
:total="neutralNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersNeutral" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.neutral">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 敌对NPC -->
|
||||
<TabsContent value="hostile" class="space-y-4 mt-6">
|
||||
<div v-if="hostileNpcs.length === 0" class="text-center py-12 text-muted-foreground">
|
||||
{{ t('diplomacy.noHostileNpcs') }}
|
||||
</div>
|
||||
<Empty v-if="hostileNpcs.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Swords class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('diplomacy.noHostileNpcs') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 md:grid-cols-2 gap-4 pb-20">
|
||||
<NpcRelationCard
|
||||
v-for="npc in paginatedHostileNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:data-npc-id="npc.id"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="space-y-2 pb-20">
|
||||
<NpcRelationRow
|
||||
v-for="npc in paginatedHostileNpcs"
|
||||
:key="npc.id"
|
||||
:ref="setCardRef(npc.id)"
|
||||
:npc="npc"
|
||||
:relation="getRelation(npc.id)"
|
||||
:class="{ 'npc-highlight': highlightedNpcId === npc.id }"
|
||||
/>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="totalPagesHostile > 1"
|
||||
v-model:page="currentPage.hostile"
|
||||
:total="hostileNpcs.length"
|
||||
:items-per-page="ITEMS_PER_PAGE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
class="mt-6"
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious>{{ t('pagination.previous') }}</PaginationPrevious>
|
||||
|
||||
<template v-for="(pageNum, index) in pageNumbersHostile" :key="index">
|
||||
<PaginationItem v-if="typeof pageNum === 'number'" :value="pageNum" :is-active="pageNum === currentPage.hostile">
|
||||
{{ pageNum }}
|
||||
</PaginationItem>
|
||||
<span v-else class="px-2 text-muted-foreground">{{ pageNum }}</span>
|
||||
</template>
|
||||
|
||||
<PaginationNext>{{ t('pagination.next') }}</PaginationNext>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 固定底部分页 -->
|
||||
<FixedPagination v-model:page="currentPageValue" :total-pages="currentTotalPages" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
@@ -320,22 +372,57 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog'
|
||||
import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue'
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
|
||||
import {
|
||||
FixedPagination,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from '@/components/ui/pagination'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import NpcRelationCard from '@/components/NpcRelationCard.vue'
|
||||
import NpcRelationRow from '@/components/NpcRelationRow.vue'
|
||||
import { RelationStatus } from '@/types/game'
|
||||
import type { DiplomaticRelation } from '@/types/game'
|
||||
import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
import { Search, Users, Heart, Minus, Swords, Activity, LayoutGrid, List } from 'lucide-vue-next'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
|
||||
const route = useRoute()
|
||||
const gameStore = useGameStore()
|
||||
const npcStore = useNPCStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref('all')
|
||||
|
||||
// 视图模式: 'card' | 'list'
|
||||
const viewMode = ref<'card' | 'list'>('list')
|
||||
|
||||
// NPC卡片引用 Map(存储组件实例)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const cardRefs = ref<Map<string, any>>(new Map())
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const setCardRef = (npcId: string) => (el: any) => {
|
||||
if (el) {
|
||||
cardRefs.value.set(npcId, el)
|
||||
} else {
|
||||
cardRefs.value.delete(npcId)
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮状态
|
||||
const highlightedNpcId = ref<string | null>(null)
|
||||
|
||||
// 搜索功能
|
||||
const searchQuery = ref('')
|
||||
|
||||
// NPC诊断功能
|
||||
const npcDiagnosticOpen = ref(false)
|
||||
const npcDiagnostics = ref<npcBehaviorLogic.NPCDiagnosticInfo[]>([])
|
||||
const diagnosticPage = ref(1)
|
||||
const diagnosticSearchQuery = ref('')
|
||||
const DIAGNOSTIC_ITEMS_PER_PAGE = 10
|
||||
const behaviorConfig = computed(() => {
|
||||
return npcBehaviorLogic.calculateDynamicBehavior(gameStore.player.points || 0)
|
||||
})
|
||||
@@ -343,9 +430,64 @@
|
||||
const showNPCDiagnostic = () => {
|
||||
const currentTime = Date.now()
|
||||
npcDiagnostics.value = npcBehaviorLogic.diagnoseNPCBehavior(npcStore.npcs, gameStore.player, currentTime)
|
||||
diagnosticPage.value = 1 // 重置分页
|
||||
diagnosticSearchQuery.value = '' // 重置搜索
|
||||
npcDiagnosticOpen.value = true
|
||||
}
|
||||
|
||||
// 诊断搜索过滤
|
||||
const filteredDiagnostics = computed(() => {
|
||||
if (!diagnosticSearchQuery.value.trim()) return npcDiagnostics.value
|
||||
const query = diagnosticSearchQuery.value.toLowerCase().trim()
|
||||
return npcDiagnostics.value.filter(d => d.npcName.toLowerCase().includes(query) || d.npcId.toLowerCase().includes(query))
|
||||
})
|
||||
|
||||
// 诊断弹窗分页
|
||||
const diagnosticTotalPages = computed(() => Math.ceil(filteredDiagnostics.value.length / DIAGNOSTIC_ITEMS_PER_PAGE))
|
||||
const paginatedDiagnostics = computed(() => {
|
||||
const start = (diagnosticPage.value - 1) * DIAGNOSTIC_ITEMS_PER_PAGE
|
||||
const end = start + DIAGNOSTIC_ITEMS_PER_PAGE
|
||||
return filteredDiagnostics.value.slice(start, end)
|
||||
})
|
||||
|
||||
const diagnosticPageNumbers = computed(() => getPageNumbers(diagnosticPage.value, diagnosticTotalPages.value))
|
||||
|
||||
// 诊断面板关系状态本地化
|
||||
const getLocalizedRelationStatus = (statusKey: string) => {
|
||||
switch (statusKey) {
|
||||
case 'friendly':
|
||||
return t('diplomacy.status.friendly')
|
||||
case 'hostile':
|
||||
return t('diplomacy.status.hostile')
|
||||
case 'neutral':
|
||||
return t('diplomacy.status.neutral')
|
||||
case 'noRelation':
|
||||
return t('diplomacy.diagnostic.noRelation')
|
||||
case 'noRelationNeutral':
|
||||
return t('diplomacy.diagnostic.noRelationNeutral')
|
||||
default:
|
||||
return t('diplomacy.status.neutral')
|
||||
}
|
||||
}
|
||||
|
||||
// 诊断面板关系Badge样式
|
||||
const getRelationBadgeVariant = (statusKey: string) => {
|
||||
switch (statusKey) {
|
||||
case 'friendly':
|
||||
return 'default'
|
||||
case 'hostile':
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 翻译诊断原因
|
||||
const translateReason = (reason: { key: string; params?: Record<string, string | number> }) => {
|
||||
const translationKey = `diplomacy.diagnostic.reasons.${reason.key}`
|
||||
return reason.params ? t(translationKey, reason.params) : t(translationKey)
|
||||
}
|
||||
|
||||
// 检测并生成NPC盟友
|
||||
const initializeNPCAllies = () => {
|
||||
const npcs = npcStore.npcs
|
||||
@@ -387,44 +529,53 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动到指定NPC卡片
|
||||
const scrollToNpcCard = (npcId: string) => {
|
||||
// 切换到"全部"标签
|
||||
activeTab.value = 'all'
|
||||
|
||||
// 等待DOM更新后再滚动
|
||||
nextTick(() => {
|
||||
// 找到目标NPC在列表中的索引
|
||||
const npcIndex = allNpcs.value.findIndex(npc => npc.id === npcId)
|
||||
if (npcIndex === -1) return
|
||||
|
||||
// 计算目标NPC所在的页面
|
||||
const targetPage = Math.floor(npcIndex / ITEMS_PER_PAGE) + 1
|
||||
currentPage.value.all = targetPage
|
||||
|
||||
// 再次等待分页更新后滚动到卡片
|
||||
nextTick(() => {
|
||||
// 从 cardRefs 获取组件实例
|
||||
const cardComponent = cardRefs.value.get(npcId)
|
||||
const targetEl = cardComponent?.$el as HTMLElement | undefined
|
||||
|
||||
if (targetEl) {
|
||||
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
// 添加高亮效果
|
||||
highlightedNpcId.value = npcId
|
||||
setTimeout(() => {
|
||||
highlightedNpcId.value = null
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时初始化NPC盟友
|
||||
onMounted(() => {
|
||||
initializeNPCAllies()
|
||||
|
||||
// 检查URL query参数,如果有npcId则滚动到该NPC
|
||||
const npcIdFromQuery = route.query.npcId as string | undefined
|
||||
if (npcIdFromQuery) {
|
||||
scrollToNpcCard(npcIdFromQuery)
|
||||
}
|
||||
|
||||
// 监听滚动到NPC卡片的事件
|
||||
const handleScrollToNpc = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ npcId: string }>
|
||||
const npcId = customEvent.detail.npcId
|
||||
|
||||
// 切换到"全部"标签
|
||||
activeTab.value = 'all'
|
||||
|
||||
// 等待DOM更新后再滚动
|
||||
nextTick(() => {
|
||||
// 找到目标NPC在列表中的索引
|
||||
const npcIndex = allNpcs.value.findIndex(npc => npc.id === npcId)
|
||||
if (npcIndex === -1) return
|
||||
|
||||
// 计算目标NPC所在的页面
|
||||
const targetPage = Math.floor(npcIndex / ITEMS_PER_PAGE) + 1
|
||||
currentPage.value.all = targetPage
|
||||
|
||||
// 再次等待分页更新后滚动到卡片
|
||||
nextTick(() => {
|
||||
// 使用data属性来标识卡片
|
||||
const cards = document.querySelectorAll('[data-npc-id]')
|
||||
const targetCard = Array.from(cards).find(card => card.getAttribute('data-npc-id') === npcId)
|
||||
|
||||
if (targetCard) {
|
||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
// 添加高亮效果
|
||||
targetCard.classList.add('ring-2', 'ring-primary', 'ring-offset-2')
|
||||
setTimeout(() => {
|
||||
targetCard.classList.remove('ring-2', 'ring-primary', 'ring-offset-2')
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
})
|
||||
scrollToNpcCard(customEvent.detail.npcId)
|
||||
}
|
||||
|
||||
document.addEventListener('scrollToNpc', handleScrollToNpc)
|
||||
@@ -444,16 +595,25 @@
|
||||
hostile: 1
|
||||
})
|
||||
|
||||
// 获取玩家对NPC的关系
|
||||
// 获取NPC对玩家的关系(统一使用 npc.relations)
|
||||
const getRelation = (npcId: string): DiplomaticRelation | undefined => {
|
||||
return gameStore.player.diplomaticRelations?.[npcId]
|
||||
const npc = npcStore.npcs.find(n => n.id === npcId)
|
||||
return npc?.relations?.[gameStore.player.id]
|
||||
}
|
||||
|
||||
// 按关系状态分类NPC
|
||||
const allNpcs = computed(() => npcStore.npcs)
|
||||
// 搜索过滤函数
|
||||
const matchesSearch = (npc: (typeof npcStore.npcs)[0]) => {
|
||||
if (!searchQuery.value.trim()) return true
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
return npc.name.toLowerCase().includes(query) || npc.id.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
// 按关系状态分类NPC(同时应用搜索过滤)
|
||||
const allNpcs = computed(() => npcStore.npcs.filter(matchesSearch))
|
||||
|
||||
const friendlyNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
if (!matchesSearch(npc)) return false
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Friendly
|
||||
})
|
||||
@@ -461,6 +621,7 @@
|
||||
|
||||
const neutralNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
if (!matchesSearch(npc)) return false
|
||||
const relation = getRelation(npc.id)
|
||||
return !relation || relation.status === RelationStatus.Neutral
|
||||
})
|
||||
@@ -468,6 +629,7 @@
|
||||
|
||||
const hostileNpcs = computed(() => {
|
||||
return npcStore.npcs.filter(npc => {
|
||||
if (!matchesSearch(npc)) return false
|
||||
const relation = getRelation(npc.id)
|
||||
return relation?.status === RelationStatus.Hostile
|
||||
})
|
||||
@@ -497,46 +659,73 @@
|
||||
const totalPagesNeutral = computed(() => getTotalPages(neutralNpcs.value))
|
||||
const totalPagesHostile = computed(() => getTotalPages(hostileNpcs.value))
|
||||
|
||||
// 生成页码列表(用于分页UI)
|
||||
// 生成页码列表(用于诊断弹窗分页UI)
|
||||
const getPageNumbers = (currentPageNum: number, totalPages: number) => {
|
||||
const pages: (number | string)[] = []
|
||||
const maxVisible = 5 // 最多显示5个页码
|
||||
const pages: number[] = []
|
||||
const maxVisible = 3
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// 如果总页数少于等于5,显示全部
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 总是显示第1页
|
||||
pages.push(1)
|
||||
let start = currentPageNum - 1
|
||||
let end = currentPageNum + 1
|
||||
|
||||
if (currentPageNum > 3) {
|
||||
pages.push('...')
|
||||
if (start < 1) {
|
||||
start = 1
|
||||
end = maxVisible
|
||||
}
|
||||
if (end > totalPages) {
|
||||
end = totalPages
|
||||
start = totalPages - maxVisible + 1
|
||||
}
|
||||
|
||||
// 计算中间显示的页码范围
|
||||
const start = Math.max(2, currentPageNum - 1)
|
||||
const end = Math.min(totalPages - 1, currentPageNum + 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (currentPageNum < totalPages - 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 总是显示最后一页
|
||||
pages.push(totalPages)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// 各标签页的页码列表
|
||||
const pageNumbersAll = computed(() => getPageNumbers(currentPage.value.all || 1, totalPagesAll.value))
|
||||
const pageNumbersFriendly = computed(() => getPageNumbers(currentPage.value.friendly || 1, totalPagesFriendly.value))
|
||||
const pageNumbersNeutral = computed(() => getPageNumbers(currentPage.value.neutral || 1, totalPagesNeutral.value))
|
||||
const pageNumbersHostile = computed(() => getPageNumbers(currentPage.value.hostile || 1, totalPagesHostile.value))
|
||||
// 当前标签页的分页数据(用于固定底部分页)
|
||||
const currentTotalPages = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'friendly':
|
||||
return totalPagesFriendly.value
|
||||
case 'neutral':
|
||||
return totalPagesNeutral.value
|
||||
case 'hostile':
|
||||
return totalPagesHostile.value
|
||||
default:
|
||||
return totalPagesAll.value
|
||||
}
|
||||
})
|
||||
|
||||
const currentPageValue = computed({
|
||||
get: () => currentPage.value[activeTab.value] || 1,
|
||||
set: (val: number) => {
|
||||
currentPage.value[activeTab.value] = val
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* NPC卡片高亮动画 - 不使用scoped以便动态添加类生效 */
|
||||
.npc-highlight {
|
||||
animation: highlight-pulse 1.5s ease-in-out 2 !important;
|
||||
box-shadow: 0 0 0 3px var(--primary) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 3px var(--primary);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 6px var(--primary), 0 0 25px var(--primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user