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>
|
||||
|
||||
@@ -8,11 +8,9 @@
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="fleet">{{ t('fleetView.fleetOverview') }}</TabsTrigger>
|
||||
<TabsTrigger value="send">{{ t('fleetView.sendFleet') }}</TabsTrigger>
|
||||
<TabsTrigger value="missions">
|
||||
{{ t('fleetView.flightMissions') }}
|
||||
<Badge v-if="gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
<TabsTrigger v-for="tab in fleetTabs" :key="tab.value" :value="tab.value">
|
||||
{{ t(`fleetView.${tab.labelKey}`) }}
|
||||
<Badge v-if="tab.value === 'missions' && gameStore.player.fleetMissions.length > 0" variant="destructive" class="ml-1">
|
||||
{{ gameStore.player.fleetMissions.length }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
@@ -97,17 +95,9 @@
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="galaxy" class="text-xs sm:text-sm">{{ t('fleetView.galaxy') }}</Label>
|
||||
<Input id="galaxy" v-model.number="targetPosition.galaxy" type="number" min="1" max="9" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="system" class="text-xs sm:text-sm">{{ t('fleetView.system') }}</Label>
|
||||
<Input id="system" v-model.number="targetPosition.system" type="number" min="1" max="10" placeholder="1" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="position" class="text-xs sm:text-sm">{{ t('fleetView.position') }}</Label>
|
||||
<Input id="position" v-model.number="targetPosition.position" type="number" min="1" max="10" placeholder="1" />
|
||||
<div v-for="coord in coordinateFields" :key="coord.key" class="space-y-2">
|
||||
<Label :for="coord.key" class="text-xs sm:text-sm">{{ t(`fleetView.${coord.key}`) }}</Label>
|
||||
<Input :id="coord.key" v-model.number="targetPosition[coord.key]" type="number" :min="1" :max="coord.max" placeholder="1" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -157,52 +147,17 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-metal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ t('resources.metal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.metal) }})
|
||||
</Label>
|
||||
<Input id="cargo-metal" v-model.number="cargo.metal" type="number" min="0" :max="planet.resources.metal" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-crystal" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ t('resources.crystal') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.crystal) }})
|
||||
<div v-for="res in cargoResourceFields" :key="res.key" class="space-y-2">
|
||||
<Label :for="`cargo-${res.key}`" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon :type="res.key" size="sm" />
|
||||
{{ t(`resources.${res.key}`) }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources[res.key]) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-crystal"
|
||||
v-model.number="cargo.crystal"
|
||||
:id="`cargo-${res.key}`"
|
||||
v-model.number="cargo[res.key]"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.crystal"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-deuterium" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ t('resources.deuterium') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.deuterium) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-deuterium"
|
||||
v-model.number="cargo.deuterium"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.deuterium"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="cargo-darkMatter" class="text-xs sm:text-sm flex items-center gap-2">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ t('resources.darkMatter') }} ({{ t('fleetView.available') }}: {{ formatNumber(planet.resources.darkMatter) }})
|
||||
</Label>
|
||||
<Input
|
||||
id="cargo-darkMatter"
|
||||
v-model.number="cargo.darkMatter"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="planet.resources.darkMatter"
|
||||
:max="planet.resources[res.key]"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
@@ -243,9 +198,12 @@
|
||||
|
||||
<!-- 飞行任务 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-4">
|
||||
<Card v-if="gameStore.player.fleetMissions.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('fleetView.noFlightMissions') }}</CardContent>
|
||||
</Card>
|
||||
<Empty v-if="gameStore.player.fleetMissions.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<RocketIcon class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('fleetView.noFlightMissions') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<Card v-for="mission in gameStore.player.fleetMissions" :key="mission.id">
|
||||
<CardHeader>
|
||||
@@ -275,25 +233,15 @@
|
||||
</div>
|
||||
|
||||
<!-- 携带资源 -->
|
||||
<div v-if="mission.cargo.metal > 0 || mission.cargo.crystal > 0 || mission.cargo.deuterium > 0 || mission.cargo.darkMatter > 0">
|
||||
<div v-if="hasCargoResources(mission.cargo)">
|
||||
<p class="text-xs sm:text-sm font-medium mb-2">{{ t('fleetView.carryingResources') }}:</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="mission.cargo.metal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="metal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.metal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.crystal > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="crystal" size="sm" />
|
||||
{{ formatNumber(mission.cargo.crystal) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.deuterium > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="deuterium" size="sm" />
|
||||
{{ formatNumber(mission.cargo.deuterium) }}
|
||||
</span>
|
||||
<span v-if="mission.cargo.darkMatter > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon type="darkMatter" size="sm" />
|
||||
{{ formatNumber(mission.cargo.darkMatter) }}
|
||||
</span>
|
||||
<template v-for="res in cargoResourceFields" :key="res.key">
|
||||
<span v-if="mission.cargo[res.key] > 0" class="flex items-center gap-1">
|
||||
<ResourceIcon :type="res.key" size="sm" />
|
||||
{{ formatNumber(mission.cargo[res.key]) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +305,7 @@
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameConfig } from '@/composables/useGameConfig'
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ShipType, MissionType, BuildingType, TechnologyType } from '@/types/game'
|
||||
import type { Fleet, Resources } from '@/types/game'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -380,6 +328,7 @@
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import UnlockRequirement from '@/components/UnlockRequirement.vue'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import { Sword, Package, Rocket as RocketIcon, Eye, Users, Recycle, Skull, Gift, Compass } from 'lucide-vue-next'
|
||||
import { formatNumber, formatTime } from '@/utils/format'
|
||||
import * as shipValidation from '@/logic/shipValidation'
|
||||
@@ -390,7 +339,6 @@
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const universeStore = useUniverseStore()
|
||||
const npcStore = useNPCStore()
|
||||
@@ -417,6 +365,13 @@
|
||||
|
||||
const activeTab = ref<'fleet' | 'send' | 'missions'>('fleet')
|
||||
|
||||
// Tab 配置
|
||||
const fleetTabs = [
|
||||
{ value: 'fleet', labelKey: 'fleetOverview' },
|
||||
{ value: 'send', labelKey: 'sendFleet' },
|
||||
{ value: 'missions', labelKey: 'flightMissions' }
|
||||
] as const
|
||||
|
||||
// 选择的舰队
|
||||
const selectedFleet = ref<Partial<Fleet>>({
|
||||
[ShipType.LightFighter]: 0,
|
||||
@@ -435,12 +390,23 @@
|
||||
// 目标坐标
|
||||
const targetPosition = ref({ galaxy: 1, system: 1, position: 1 })
|
||||
|
||||
// 坐标字段配置
|
||||
const coordinateFields: { key: keyof typeof targetPosition.value; max: number }[] = [
|
||||
{ key: 'galaxy', max: 9 },
|
||||
{ key: 'system', max: 10 },
|
||||
{ key: 'position', max: 10 }
|
||||
]
|
||||
|
||||
// 选择的任务类型
|
||||
const selectedMission = ref<MissionType>(MissionType.Attack)
|
||||
|
||||
// 运输资源
|
||||
const cargo = ref({ metal: 0, crystal: 0, deuterium: 0, darkMatter: 0, energy: 0 })
|
||||
|
||||
// 货物资源字段配置
|
||||
type CargoKey = 'metal' | 'crystal' | 'deuterium' | 'darkMatter'
|
||||
const cargoResourceFields: { key: CargoKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }, { key: 'darkMatter' }]
|
||||
|
||||
// 从 URL query 参数初始化
|
||||
onMounted(() => {
|
||||
// 启动定时器更新当前时间
|
||||
@@ -472,9 +438,6 @@
|
||||
|
||||
// 自动切换到派遣舰队标签
|
||||
activeTab.value = 'send'
|
||||
|
||||
// 清除 URL 参数,保持 URL 整洁
|
||||
router.replace({ path: '/fleet' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -553,6 +516,11 @@
|
||||
return cargo.value.metal + cargo.value.crystal + cargo.value.deuterium + cargo.value.darkMatter
|
||||
}
|
||||
|
||||
// 检查是否有携带资源
|
||||
const hasCargoResources = (cargoData: Resources): boolean => {
|
||||
return cargoData.metal > 0 || cargoData.crystal > 0 || cargoData.deuterium > 0 || cargoData.darkMatter > 0
|
||||
}
|
||||
|
||||
// 计算燃料消耗(包含货物重量影响)
|
||||
const getFuelConsumption = (): number => {
|
||||
const bonuses = officerLogic.calculateActiveBonuses(gameStore.player.officers, Date.now())
|
||||
|
||||
@@ -77,110 +77,36 @@
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 建筑 -->
|
||||
<TabsContent value="buildings" class="space-y-4">
|
||||
<!-- 建筑/科技/舰船/防御/军官 - 统一配置渲染 -->
|
||||
<TabsContent v-for="section in gmSections" :key="section.tabValue" :value="section.tabValue" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyBuildings') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.buildingsDesc') }}</CardDescription>
|
||||
<CardTitle>{{ t(section.titleKey) }}</CardTitle>
|
||||
<CardDescription>{{ t(section.descKey) }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="building in buildingTypes" :key="building" class="space-y-2">
|
||||
<Label>{{ BUILDINGS[building].name }}</Label>
|
||||
<div v-for="item in section.items" :key="item" class="space-y-2">
|
||||
<Label>{{ section.getItemName(item) }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.buildings[building]" type="number" min="0" max="100" class="flex-1" />
|
||||
<Button @click="setBuildingLevel(building, 10)" variant="outline" size="sm">Lv 10</Button>
|
||||
<Button @click="setBuildingLevel(building, 30)" variant="outline" size="sm">Lv 30</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 科技 -->
|
||||
<TabsContent value="research" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyResearch') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.researchDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="tech in technologyTypes" :key="tech" class="space-y-2">
|
||||
<Label>{{ TECHNOLOGIES[tech].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="gameStore.player.technologies[tech]" type="number" min="0" max="50" class="flex-1" />
|
||||
<Button @click="setTechnologyLevel(tech, 10)" variant="outline" size="sm">Lv 10</Button>
|
||||
<Button @click="setTechnologyLevel(tech, 20)" variant="outline" size="sm">Lv 20</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 舰船 -->
|
||||
<TabsContent value="ships" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyShips') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.shipsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="ship in shipTypes" :key="ship" class="space-y-2">
|
||||
<Label>{{ SHIPS[ship].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.fleet[ship]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setShipCount(ship, 100)" variant="outline" size="sm">+100</Button>
|
||||
<Button @click="setShipCount(ship, 1000)" variant="outline" size="sm">+1K</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 防御 -->
|
||||
<TabsContent value="defense" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyDefense') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.defenseDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="defense in defenseTypes" :key="defense" class="space-y-2">
|
||||
<Label>{{ DEFENSES[defense].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="selectedPlanet.defense[defense]" type="number" min="0" class="flex-1" />
|
||||
<Button @click="setDefenseCount(defense, 100)" variant="outline" size="sm">+100</Button>
|
||||
<Button @click="setDefenseCount(defense, 1000)" variant="outline" size="sm">+1K</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 军官 -->
|
||||
<TabsContent value="officers" class="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('gmView.modifyOfficers') }}</CardTitle>
|
||||
<CardDescription>{{ t('gmView.officersDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="officer in officerTypes" :key="officer" class="space-y-2">
|
||||
<Label>{{ OFFICERS[officer].name }}</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input v-model.number="officerDays[officer]" type="number" min="0" :placeholder="t('gmView.days')" class="flex-1" />
|
||||
<Button @click="setOfficerDays(officer, 7)" variant="outline" size="sm">7{{ t('gmView.days') }}</Button>
|
||||
<Button @click="setOfficerDays(officer, 30)" variant="outline" size="sm">30{{ t('gmView.days') }}</Button>
|
||||
<Button @click="setOfficerDays(officer, 365)" variant="outline" size="sm">365{{ t('gmView.days') }}</Button>
|
||||
<Input
|
||||
:model-value="section.getValue(item)"
|
||||
@update:model-value="section.setValue(item, Number($event) || 0)"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="section.max"
|
||||
:placeholder="section.placeholder"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Button
|
||||
v-for="btn in section.buttons"
|
||||
:key="btn.label"
|
||||
@click="section.onButtonClick(item, btn.value)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +130,9 @@
|
||||
<SelectValue :placeholder="t('gmView.chooseNPC') || 'Choose NPC'" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">{{ npc.name }} ({{ npc.difficulty }})</SelectItem>
|
||||
<SelectItem v-for="npc in npcStore.npcs" :key="npc.id" :value="npc.id">
|
||||
{{ npc.name }} ({{ t(`diplomacy.diagnostic.difficultyLevels.${npc.difficulty}`) }})
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -293,6 +221,9 @@
|
||||
<AlertDialogDescription v-if="alertDialogMessage" class="whitespace-pre-line">
|
||||
{{ alertDialogMessage }}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription v-else class="sr-only">
|
||||
{{ alertDialogTitle }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction @click="handleAlertConfirm">{{ t('common.confirm') }}</AlertDialogAction>
|
||||
@@ -388,11 +319,6 @@
|
||||
})
|
||||
|
||||
const resourceTypes = ['metal', 'crystal', 'deuterium', 'darkMatter'] as const
|
||||
const buildingTypes = Object.values(BuildingType)
|
||||
const technologyTypes = Object.values(TechnologyType)
|
||||
const shipTypes = Object.values(ShipType)
|
||||
const defenseTypes = Object.values(DefenseType)
|
||||
const officerTypes = Object.values(OfficerType)
|
||||
|
||||
// Tab配置
|
||||
const tabs = [
|
||||
@@ -410,52 +336,161 @@
|
||||
}
|
||||
}
|
||||
|
||||
const setBuildingLevel = (building: BuildingType, level: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.buildings[building] = level
|
||||
updatePlayerPoints()
|
||||
}
|
||||
// GM编辑区块配置 - 统一管理建筑/科技/舰船/防御/军官
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type GMSection = {
|
||||
tabValue: string
|
||||
titleKey: string
|
||||
descKey: string
|
||||
items: string[]
|
||||
max?: number
|
||||
placeholder?: string
|
||||
buttons: { label: string; value: number }[]
|
||||
getItemName: (item: any) => string
|
||||
getValue: (item: any) => number
|
||||
setValue: (item: any, val: number) => void
|
||||
onButtonClick: (item: any, val: number) => void
|
||||
}
|
||||
|
||||
const setTechnologyLevel = (tech: TechnologyType, level: number) => {
|
||||
gameStore.player.technologies[tech] = level
|
||||
updatePlayerPoints()
|
||||
}
|
||||
|
||||
const setShipCount = (ship: ShipType, count: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[ship] = (selectedPlanet.value.fleet[ship] || 0) + count
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setDefenseCount = (defense: DefenseType, count: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[defense] = (selectedPlanet.value.defense[defense] || 0) + count
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
|
||||
const setOfficerDays = (officer: OfficerType, days: number) => {
|
||||
officerDays.value[officer] = days
|
||||
const now = Date.now()
|
||||
const expiresAt = now + days * 24 * 60 * 60 * 1000
|
||||
|
||||
if (!gameStore.player.officers[officer]) {
|
||||
gameStore.player.officers[officer] = {
|
||||
type: officer,
|
||||
active: true,
|
||||
hiredAt: now,
|
||||
expiresAt: expiresAt
|
||||
const gmSections = computed<GMSection[]>(() => [
|
||||
{
|
||||
tabValue: 'buildings',
|
||||
titleKey: 'gmView.modifyBuildings',
|
||||
descKey: 'gmView.buildingsDesc',
|
||||
items: Object.values(BuildingType),
|
||||
max: 100,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: 'Lv 10', value: 10 },
|
||||
{ label: 'Lv 30', value: 30 }
|
||||
],
|
||||
getItemName: (item: BuildingType) => BUILDINGS.value[item].name,
|
||||
getValue: (item: BuildingType) => selectedPlanet.value?.buildings[item] || 0,
|
||||
setValue: (item: BuildingType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.buildings[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
onButtonClick: (item: BuildingType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.buildings[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gameStore.player.officers[officer].expiresAt = expiresAt
|
||||
gameStore.player.officers[officer].active = true
|
||||
if (!gameStore.player.officers[officer].hiredAt) {
|
||||
gameStore.player.officers[officer].hiredAt = now
|
||||
},
|
||||
{
|
||||
tabValue: 'research',
|
||||
titleKey: 'gmView.modifyResearch',
|
||||
descKey: 'gmView.researchDesc',
|
||||
items: Object.values(TechnologyType),
|
||||
max: 50,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: 'Lv 10', value: 10 },
|
||||
{ label: 'Lv 20', value: 20 }
|
||||
],
|
||||
getItemName: (item: TechnologyType) => TECHNOLOGIES.value[item].name,
|
||||
getValue: (item: TechnologyType) => gameStore.player.technologies[item] || 0,
|
||||
setValue: (item: TechnologyType, val: number) => {
|
||||
gameStore.player.technologies[item] = val
|
||||
updatePlayerPoints()
|
||||
},
|
||||
onButtonClick: (item: TechnologyType, val: number) => {
|
||||
gameStore.player.technologies[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
{
|
||||
tabValue: 'ships',
|
||||
titleKey: 'gmView.modifyShips',
|
||||
descKey: 'gmView.shipsDesc',
|
||||
items: Object.values(ShipType),
|
||||
max: undefined,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: '+100', value: 100 },
|
||||
{ label: '+1K', value: 1000 }
|
||||
],
|
||||
getItemName: (item: ShipType) => SHIPS.value[item].name,
|
||||
getValue: (item: ShipType) => selectedPlanet.value?.fleet[item] || 0,
|
||||
setValue: (item: ShipType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
onButtonClick: (item: ShipType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.fleet[item] = (selectedPlanet.value.fleet[item] || 0) + val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tabValue: 'defense',
|
||||
titleKey: 'gmView.modifyDefense',
|
||||
descKey: 'gmView.defenseDesc',
|
||||
items: Object.values(DefenseType),
|
||||
max: undefined,
|
||||
placeholder: undefined,
|
||||
buttons: [
|
||||
{ label: '+100', value: 100 },
|
||||
{ label: '+1K', value: 1000 }
|
||||
],
|
||||
getItemName: (item: DefenseType) => DEFENSES.value[item].name,
|
||||
getValue: (item: DefenseType) => selectedPlanet.value?.defense[item] || 0,
|
||||
setValue: (item: DefenseType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[item] = val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
},
|
||||
onButtonClick: (item: DefenseType, val: number) => {
|
||||
if (selectedPlanet.value) {
|
||||
selectedPlanet.value.defense[item] = (selectedPlanet.value.defense[item] || 0) + val
|
||||
updatePlayerPoints()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tabValue: 'officers',
|
||||
titleKey: 'gmView.modifyOfficers',
|
||||
descKey: 'gmView.officersDesc',
|
||||
items: Object.values(OfficerType),
|
||||
max: undefined,
|
||||
placeholder: t('gmView.days'),
|
||||
buttons: [
|
||||
{ label: `7${t('gmView.days')}`, value: 7 },
|
||||
{ label: `30${t('gmView.days')}`, value: 30 },
|
||||
{ label: `365${t('gmView.days')}`, value: 365 }
|
||||
],
|
||||
getItemName: (item: OfficerType) => OFFICERS.value[item].name,
|
||||
getValue: (item: OfficerType) => officerDays.value[item] || 0,
|
||||
setValue: (item: OfficerType, val: number) => {
|
||||
officerDays.value[item] = val
|
||||
},
|
||||
onButtonClick: (item: OfficerType, days: number) => {
|
||||
officerDays.value[item] = days
|
||||
const now = Date.now()
|
||||
const expiresAt = now + days * 24 * 60 * 60 * 1000
|
||||
if (!gameStore.player.officers[item]) {
|
||||
gameStore.player.officers[item] = {
|
||||
type: item,
|
||||
active: true,
|
||||
hiredAt: now,
|
||||
expiresAt: expiresAt
|
||||
}
|
||||
} else {
|
||||
gameStore.player.officers[item].expiresAt = expiresAt
|
||||
gameStore.player.officers[item].active = true
|
||||
if (!gameStore.player.officers[item].hiredAt) {
|
||||
gameStore.player.officers[item].hiredAt = now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 显示重置游戏确认对话框
|
||||
const showResetConfirmDialog = () => {
|
||||
|
||||
@@ -205,16 +205,30 @@
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<!-- 第一行:名称、坐标、状态、残骸 -->
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-wrap">
|
||||
<h3 class="font-semibold text-sm truncate">{{ slot.planet.name }}</h3>
|
||||
<h3 class="font-semibold text-sm truncate">
|
||||
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||
</h3>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</span>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs flex-shrink-0">
|
||||
{{ t('galaxyView.mine') }}
|
||||
</Badge>
|
||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
<Popover v-else>
|
||||
<PopoverTrigger as-child>
|
||||
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs flex-shrink-0 cursor-pointer">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent v-if="getReputationValue(slot.planet) !== null" class="w-auto p-3" side="top" align="center">
|
||||
<p class="text-sm">
|
||||
{{ t('diplomacy.reputation') }}:
|
||||
<span :class="getReputationColor(getReputationValue(slot.planet))" class="font-medium">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<PopoverTrigger as-child>
|
||||
<Badge
|
||||
@@ -224,7 +238,7 @@
|
||||
<Recycle class="h-3 w-3" />
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-3" side="top" align="start">
|
||||
<PopoverContent class="w-auto p-3" side="top" align="center">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold text-amber-700 dark:text-amber-400">{{ t('galaxyView.debrisField') }}</p>
|
||||
<div class="space-y-1 text-xs">
|
||||
@@ -247,13 +261,6 @@
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<!-- 第二行:好感度 -->
|
||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空位置 -->
|
||||
<div v-else class="space-y-1">
|
||||
@@ -389,11 +396,27 @@
|
||||
<div v-if="slot.planet" class="space-y-1">
|
||||
<!-- PC端:标题和徽章 -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h3 class="font-semibold text-base">{{ slot.planet.name }}</h3>
|
||||
<h3 class="font-semibold text-base">
|
||||
{{ isMyPlanet(slot.planet) ? slot.planet.name : getNpcPlanetDisplayName(slot.planet) }}
|
||||
</h3>
|
||||
<Badge v-if="isMyPlanet(slot.planet)" variant="default" class="text-xs">{{ t('galaxyView.mine') }}</Badge>
|
||||
<Badge v-else :variant="getRelationBadgeVariant(slot.planet)" class="text-xs">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
<TooltipProvider v-else :delay-duration="300">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Badge :variant="getRelationBadgeVariant(slot.planet)" class="text-xs cursor-default">
|
||||
{{ getRelationStatusText(slot.planet) }}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent v-if="getReputationValue(slot.planet) !== null">
|
||||
<p>
|
||||
{{ t('diplomacy.reputation') }}:
|
||||
<span :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<!-- 残骸场徽章 -->
|
||||
<Popover v-if="getDebrisFieldAt(currentGalaxy, currentSystem, slot.position)">
|
||||
<PopoverTrigger as-child>
|
||||
@@ -432,13 +455,6 @@
|
||||
<p class="text-xs text-muted-foreground">
|
||||
[{{ slot.planet.position.galaxy }}:{{ slot.planet.position.system }}:{{ slot.planet.position.position }}]
|
||||
</p>
|
||||
<!-- PC端:好感度显示(仅NPC星球) -->
|
||||
<div v-if="!isMyPlanet(slot.planet) && getReputationValue(slot.planet) !== null" class="text-xs">
|
||||
<span class="text-muted-foreground">{{ t('diplomacy.reputation') }}:</span>
|
||||
<span class="ml-1 font-semibold" :class="getReputationColor(getReputationValue(slot.planet))">
|
||||
{{ getReputationValue(slot.planet)! > 0 ? '+' : '' }}{{ getReputationValue(slot.planet) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空位置 -->
|
||||
<div v-else class="space-y-1">
|
||||
@@ -744,9 +760,6 @@
|
||||
selectedGalaxy.value = queryGalaxy
|
||||
selectedSystem.value = querySystem
|
||||
loadSystem()
|
||||
|
||||
// 立即清除URL参数,但保持本地变量中的highlightNpcId
|
||||
clearUrlParams()
|
||||
} else if (gameStore.currentPlanet) {
|
||||
// 否则默认显示当前星球所在的星系
|
||||
currentGalaxy.value = gameStore.currentPlanet.position.galaxy
|
||||
@@ -778,13 +791,6 @@
|
||||
return universeStore.debrisFields[debrisId] || null
|
||||
}
|
||||
|
||||
// 清除URL参数
|
||||
const clearUrlParams = () => {
|
||||
if (route.query.highlightNpc || route.query.galaxy || route.query.system) {
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
}
|
||||
|
||||
// 加载星系
|
||||
const loadSystem = () => {
|
||||
currentGalaxy.value = selectedGalaxy.value
|
||||
@@ -833,7 +839,8 @@
|
||||
const getRelation = (planet: Planet | null) => {
|
||||
const npc = getPlanetNPC(planet)
|
||||
if (!npc) return null
|
||||
return gameStore.player.diplomaticRelations?.[npc.id]
|
||||
// 从NPC的relations中获取对玩家的关系
|
||||
return npc.relations?.[gameStore.player.id]
|
||||
}
|
||||
|
||||
// 获取关系状态Badge样式
|
||||
@@ -880,6 +887,17 @@
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
|
||||
// 获取NPC星球的显示名称 - 使用"XXX的星球"格式,如果有备注则显示"NPC名称(备注)的星球"
|
||||
const getNpcPlanetDisplayName = (planet: Planet | null): string => {
|
||||
if (!planet) return ''
|
||||
const npc = getPlanetNPC(planet)
|
||||
if (npc) {
|
||||
const displayName = npc.note ? `${npc.name}(${npc.note})` : npc.name
|
||||
return t('galaxyView.npcPlanetName').replace('{name}', displayName)
|
||||
}
|
||||
return planet.name
|
||||
}
|
||||
|
||||
// 切换到指定星球
|
||||
const switchToPlanet = (planetId: string) => {
|
||||
gameStore.currentPlanetId = planetId
|
||||
|
||||
159
src/views/HomeView.vue
Normal file
159
src/views/HomeView.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-background to-muted/50 p-4">
|
||||
<!-- Logo 和标题 -->
|
||||
<div class="text-center mb-8 animate-fade-in">
|
||||
<img src="@/assets/logo.svg" alt="OGame Logo" class="w-24 h-24 mx-auto mb-4" />
|
||||
<h1 class="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||
{{ pkg.title }}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-2 text-sm sm:text-base">{{ t('home.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 开始游戏按钮 -->
|
||||
<div class="flex flex-col gap-4 w-full max-w-xs mb-8">
|
||||
<Button size="lg" class="w-full text-lg h-14" @click="handleStartGame">
|
||||
<Rocket class="mr-2 h-5 w-5" />
|
||||
{{ t('home.startGame') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-4">
|
||||
<!-- 语言切换 -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm">
|
||||
<Languages class="mr-2 h-4 w-4" />
|
||||
{{ localeNames[gameStore.locale] }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-48 p-2" side="top">
|
||||
<div class="space-y-1">
|
||||
<Button
|
||||
v-for="locale in availableLocales"
|
||||
:key="locale"
|
||||
@click="gameStore.locale = locale"
|
||||
:variant="gameStore.locale === locale ? 'secondary' : 'ghost'"
|
||||
class="w-full justify-start"
|
||||
size="sm"
|
||||
>
|
||||
{{ localeNames[locale] }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- 隐私协议按钮 -->
|
||||
<Button variant="ghost" size="sm" @click="showPrivacyDialog = true">
|
||||
<Shield class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.privacyPolicy') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 隐私协议弹窗 -->
|
||||
<PrivacyDialog v-model:open="showPrivacyDialog" />
|
||||
|
||||
<!-- 隐私协议同意确认弹窗(开始游戏时) -->
|
||||
<AlertDialog v-model:open="showAgreementDialog">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ t('home.privacyAgreement') }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{{ t('home.privacyAgreementDesc') }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div class="my-4 max-h-48 overflow-y-auto text-sm text-muted-foreground border rounded-lg p-3">
|
||||
<p>{{ t('privacy.sections.introduction.content') }}</p>
|
||||
<p class="mt-2">{{ t('privacy.sections.dataCollection.content') }}</p>
|
||||
<p class="mt-2">{{ t('privacy.sections.thirdParty.content') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Checkbox id="privacy-agree" v-model="privacyAgreed" />
|
||||
<label for="privacy-agree" class="text-sm cursor-pointer">
|
||||
{{ t('home.agreeToPrivacy') }}
|
||||
<Button variant="link" class="p-0 h-auto text-sm" @click.prevent="showPrivacyDialog = true">
|
||||
{{ t('home.viewFullPolicy') }}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="privacyAgreed = false">{{ t('common.cancel') }}</AlertDialogCancel>
|
||||
<AlertDialogAction :disabled="!privacyAgreed" @click="confirmStartGame">
|
||||
{{ t('home.agreeAndStart') }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { localeNames, type Locale } from '@/locales'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Rocket, Languages, Shield } from 'lucide-vue-next'
|
||||
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const showPrivacyDialog = ref(false)
|
||||
const showAgreementDialog = ref(false)
|
||||
const privacyAgreed = ref(false)
|
||||
|
||||
const availableLocales: Locale[] = ['zh-CN', 'zh-TW', 'en', 'de', 'ru', 'ko', 'ja']
|
||||
|
||||
// 处理开始游戏
|
||||
const handleStartGame = () => {
|
||||
// 如果已经同意过,直接进入总览页面
|
||||
if (gameStore.player.privacyAgreed) {
|
||||
router.push('/overview')
|
||||
} else {
|
||||
// 显示同意弹窗
|
||||
showAgreementDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 确认开始游戏
|
||||
const confirmStartGame = () => {
|
||||
if (privacyAgreed.value) {
|
||||
gameStore.player.privacyAgreed = true
|
||||
showAgreementDialog.value = false
|
||||
router.push('/overview')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,36 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4 sm:p-6 space-y-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">{{ t('messagesView.title') }}</h1>
|
||||
|
||||
<!-- 清空消息按钮 -->
|
||||
<Popover v-model:open="showClearPopover">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="gap-2">
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{{ t('messagesView.clearMessages') }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-80">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-medium leading-none">{{ t('messagesView.clearMessageTypes') }}</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div v-for="option in clearOptionFields" :key="option.key" class="flex items-center space-x-2">
|
||||
<Checkbox :id="`clear-${option.key}`" v-model="clearOptions[option.key]" />
|
||||
<label :for="`clear-${option.key}`" class="text-sm cursor-pointer">
|
||||
{{ t(`messagesView.${option.labelKey}`) }} ({{ option.count }})
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button @click="clearSelectedMessages" class="w-full" :disabled="!hasSelectedAny">
|
||||
{{ t('messagesView.clearNow') }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
@@ -8,17 +38,20 @@
|
||||
<TabsTrigger v-for="tab in tabs" :key="tab.value" :value="tab.value" class="flex items-center justify-center gap-1 px-2">
|
||||
<component :is="tab.icon" class="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
<span class="text-xs sm:text-sm truncate">{{ tab.label }}</span>
|
||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="hidden sm:flex ml-1">
|
||||
<Badge v-if="tab.unreadCount > 0" variant="destructive" class="ml-1 text-xs">
|
||||
{{ tab.unreadCount }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 战斗报告列表 -->
|
||||
<TabsContent value="battles" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.battleReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noBattleReports') }}</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="battles" class="mt-4 space-y-2 pb-20">
|
||||
<Empty v-if="gameStore.player.battleReports.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Sword class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noBattleReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedBattleReports"
|
||||
@@ -48,10 +81,13 @@
|
||||
</TabsContent>
|
||||
|
||||
<!-- 间谍报告列表(合并:侦查报告 + 被侦查通知) -->
|
||||
<TabsContent value="spy" class="mt-4 space-y-2">
|
||||
<Card v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noSpyReports') }}</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="spy" class="mt-4 space-y-2 pb-20">
|
||||
<Empty v-if="gameStore.player.spyReports.length === 0 && sortedSpiedNotifications.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Eye class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noSpyReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<!-- 侦查报告 -->
|
||||
<Card
|
||||
@@ -107,16 +143,20 @@
|
||||
</TabsContent>
|
||||
|
||||
<!-- NPC相关消息(活动、礼物、被拒绝) -->
|
||||
<TabsContent value="npc" class="mt-4 space-y-2">
|
||||
<Card
|
||||
<TabsContent value="npc" class="mt-4 space-y-2 pb-20">
|
||||
<Empty
|
||||
v-if="
|
||||
sortedNPCActivityNotifications.length === 0 &&
|
||||
sortedGiftNotifications.length === 0 &&
|
||||
sortedGiftRejectedNotifications.length === 0
|
||||
"
|
||||
class="border rounded-lg"
|
||||
>
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noNPCActivity') }}</CardContent>
|
||||
</Card>
|
||||
<EmptyContent>
|
||||
<Users class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noNPCActivity') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<!-- NPC活动通知 -->
|
||||
<Card
|
||||
@@ -177,11 +217,11 @@
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.giftResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="gift.resources.metal > 0">{{ t('resources.metal') }}: {{ gift.resources.metal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.crystal > 0">{{ t('resources.crystal') }}: {{ gift.resources.crystal.toLocaleString() }}</div>
|
||||
<div v-if="gift.resources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ gift.resources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
<template v-for="res in basicResourceFields" :key="res.key">
|
||||
<div v-if="gift.resources[res.key] > 0">
|
||||
{{ t(`resources.${res.key}`) }}: {{ gift.resources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
@@ -229,15 +269,11 @@
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">{{ t('messagesView.rejectedResources') }}:</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="rejection.rejectedResources.metal > 0">
|
||||
{{ t('resources.metal') }}: {{ rejection.rejectedResources.metal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.crystal > 0">
|
||||
{{ t('resources.crystal') }}: {{ rejection.rejectedResources.crystal.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="rejection.rejectedResources.deuterium > 0">
|
||||
{{ t('resources.deuterium') }}: {{ rejection.rejectedResources.deuterium.toLocaleString() }}
|
||||
</div>
|
||||
<template v-for="res in basicResourceFields" :key="res.key">
|
||||
<div v-if="rejection.rejectedResources[res.key] > 0">
|
||||
{{ t(`resources.${res.key}`) }}: {{ rejection.rejectedResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
@@ -253,10 +289,13 @@
|
||||
</TabsContent>
|
||||
|
||||
<!-- 任务报告列表 -->
|
||||
<TabsContent value="missions" class="mt-4 space-y-2">
|
||||
<Card v-if="sortedMissionReports.length === 0">
|
||||
<CardContent class="py-8 text-center text-muted-foreground">{{ t('messagesView.noMissionReports') }}</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="missions" class="mt-4 space-y-2 pb-20">
|
||||
<Empty v-if="sortedMissionReports.length === 0" class="border rounded-lg">
|
||||
<EmptyContent>
|
||||
<Package class="h-10 w-10 text-muted-foreground" />
|
||||
<EmptyDescription>{{ t('messagesView.noMissionReports') }}</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
||||
<Card
|
||||
v-for="report in sortedMissionReports"
|
||||
@@ -291,6 +330,9 @@
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<!-- 固定底部分页 -->
|
||||
<FixedPagination v-model:page="currentPage[activeTab]" :total-pages="getPaginationConfig(activeTab).totalPages" />
|
||||
|
||||
<!-- 战斗报告对话框 -->
|
||||
<BattleReportDialog v-model:open="showBattleDialog" :report="selectedBattleReport" />
|
||||
|
||||
@@ -305,6 +347,9 @@
|
||||
<Eye class="h-5 w-5 text-purple-500" />
|
||||
{{ t('messagesView.spiedNotificationDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.spyDetected') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedSpiedNotification" class="space-y-4">
|
||||
@@ -370,6 +415,9 @@
|
||||
<component :is="getMissionIcon(selectedMissionReport?.missionType)" class="h-5 w-5" />
|
||||
{{ t('messagesView.missionReportDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.missionDetails') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedMissionReport" class="space-y-4">
|
||||
@@ -417,10 +465,8 @@
|
||||
<div v-if="selectedMissionReport.details?.transportedResources" class="mt-3 space-y-1">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.transportedResources') }}:</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.transportedResources.metal.toLocaleString() }}</div>
|
||||
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.transportedResources.crystal.toLocaleString() }}</div>
|
||||
<div>
|
||||
{{ t('resources.deuterium') }}: {{ selectedMissionReport.details.transportedResources.deuterium.toLocaleString() }}
|
||||
<div v-for="res in basicResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.transportedResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,14 +475,16 @@
|
||||
<div v-if="selectedMissionReport.details?.recycledResources" class="mt-3 space-y-1">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.recycledResources') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.recycledResources.metal.toLocaleString() }}</div>
|
||||
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.recycledResources.crystal.toLocaleString() }}</div>
|
||||
<div v-for="res in debrisResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.recycledResources[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMissionReport.details.remainingDebris" class="mt-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('messagesView.remainingDebris') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-sm text-yellow-600 dark:text-yellow-400">
|
||||
<div>{{ t('resources.metal') }}: {{ selectedMissionReport.details.remainingDebris.metal.toLocaleString() }}</div>
|
||||
<div>{{ t('resources.crystal') }}: {{ selectedMissionReport.details.remainingDebris.crystal.toLocaleString() }}</div>
|
||||
<div v-for="res in debrisResourceFields" :key="res.key">
|
||||
{{ t(`resources.${res.key}`) }}: {{ selectedMissionReport.details.remainingDebris[res.key].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -449,6 +497,34 @@
|
||||
<span class="font-medium">{{ selectedMissionReport.details.newPlanetName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导弹攻击详情 -->
|
||||
<div v-if="selectedMissionReport.details?.missileCount !== undefined" class="mt-3 space-y-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.missileAttack') }}:</p>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('galaxyView.missileCount') }}:</span>
|
||||
<span class="ml-1 font-medium">{{ selectedMissionReport.details.missileCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('missionReports.hits') }}:</span>
|
||||
<span class="ml-1 font-medium text-green-600">{{ selectedMissionReport.details.missileHits }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">{{ t('galaxyView.intercepted') }}:</span>
|
||||
<span class="ml-1 font-medium text-yellow-600">{{ selectedMissionReport.details.missileIntercepted }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Object.keys(selectedMissionReport.details.defenseLosses || {}).length > 0" class="mt-2">
|
||||
<p class="text-xs font-semibold text-muted-foreground">{{ t('galaxyView.defenseLosses') }}:</p>
|
||||
<div class="grid grid-cols-2 gap-1 text-xs mt-1 p-2 bg-red-50 dark:bg-red-950/30 rounded">
|
||||
<div v-for="(count, defenseType) in selectedMissionReport.details.defenseLosses" :key="defenseType">
|
||||
<span class="text-muted-foreground">{{ t('defenses.' + defenseType) }}:</span>
|
||||
<span class="ml-1 font-medium text-red-600 dark:text-red-400">-{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,6 +543,9 @@
|
||||
<Recycle class="h-5 w-5 text-yellow-500" />
|
||||
{{ t('messagesView.npcActivityDetails') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ t('messagesView.activityDescription') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="selectedNPCActivityNotification" class="space-y-4">
|
||||
@@ -551,11 +630,15 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
|
||||
import { FixedPagination } from '@/components/ui/pagination'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import BattleReportDialog from '@/components/BattleReportDialog.vue'
|
||||
import SpyReportDialog from '@/components/SpyReportDialog.vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass } from 'lucide-vue-next'
|
||||
import { X, Sword, Eye, AlertTriangle, Package, Recycle, Gift, Ban, Check, Users, Skull, Globe, Compass, Trash2 } from 'lucide-vue-next'
|
||||
import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty'
|
||||
import type {
|
||||
BattleResult,
|
||||
SpyReport,
|
||||
@@ -568,6 +651,7 @@
|
||||
import { MissionType } from '@/types/game'
|
||||
import { useNPCStore } from '@/stores/npcStore'
|
||||
import * as diplomaticLogic from '@/logic/diplomaticLogic'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const router = useRouter()
|
||||
const gameStore = useGameStore()
|
||||
@@ -575,6 +659,107 @@
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref<'battles' | 'spy' | 'missions' | 'npc'>('battles')
|
||||
|
||||
// 清空消息功能
|
||||
const showClearPopover = ref(false)
|
||||
type ClearOptionKey =
|
||||
| 'battles'
|
||||
| 'spyReports'
|
||||
| 'spiedNotifications'
|
||||
| 'missionReports'
|
||||
| 'npcActivity'
|
||||
| 'giftNotifications'
|
||||
| 'giftRejected'
|
||||
const clearOptions = ref<Record<ClearOptionKey, boolean>>({
|
||||
battles: false,
|
||||
spyReports: false,
|
||||
spiedNotifications: false,
|
||||
missionReports: false,
|
||||
npcActivity: false,
|
||||
giftNotifications: false,
|
||||
giftRejected: false
|
||||
})
|
||||
|
||||
// 清空消息选项配置
|
||||
const clearOptionFields = computed(() => [
|
||||
{ key: 'battles' as ClearOptionKey, labelKey: 'clearBattleReports', count: gameStore.player.battleReports.length },
|
||||
{ key: 'spyReports' as ClearOptionKey, labelKey: 'clearSpyReports', count: gameStore.player.spyReports.length },
|
||||
{
|
||||
key: 'spiedNotifications' as ClearOptionKey,
|
||||
labelKey: 'clearSpiedNotifications',
|
||||
count: gameStore.player.spiedNotifications?.length || 0
|
||||
},
|
||||
{ key: 'missionReports' as ClearOptionKey, labelKey: 'clearMissionReports', count: gameStore.player.missionReports?.length || 0 },
|
||||
{ key: 'npcActivity' as ClearOptionKey, labelKey: 'clearNPCActivity', count: gameStore.player.npcActivityNotifications?.length || 0 },
|
||||
{
|
||||
key: 'giftNotifications' as ClearOptionKey,
|
||||
labelKey: 'clearGiftNotifications',
|
||||
count: gameStore.player.giftNotifications?.length || 0
|
||||
},
|
||||
{ key: 'giftRejected' as ClearOptionKey, labelKey: 'clearGiftRejected', count: gameStore.player.giftRejectedNotifications?.length || 0 }
|
||||
])
|
||||
|
||||
// 基础资源字段配置(用于显示资源列表)
|
||||
type BasicResourceKey = 'metal' | 'crystal' | 'deuterium'
|
||||
const basicResourceFields: { key: BasicResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }, { key: 'deuterium' }]
|
||||
|
||||
// 残骸资源字段配置(只有金属和晶体)
|
||||
type DebrisResourceKey = 'metal' | 'crystal'
|
||||
const debrisResourceFields: { key: DebrisResourceKey }[] = [{ key: 'metal' }, { key: 'crystal' }]
|
||||
|
||||
const hasSelectedAny = computed(() => {
|
||||
return Object.values(clearOptions.value).some(v => v)
|
||||
})
|
||||
|
||||
const clearSelectedMessages = () => {
|
||||
if (clearOptions.value.battles) {
|
||||
gameStore.player.battleReports = []
|
||||
}
|
||||
if (clearOptions.value.spyReports) {
|
||||
gameStore.player.spyReports = []
|
||||
}
|
||||
if (clearOptions.value.spiedNotifications) {
|
||||
gameStore.player.spiedNotifications = []
|
||||
}
|
||||
if (clearOptions.value.missionReports) {
|
||||
gameStore.player.missionReports = []
|
||||
}
|
||||
if (clearOptions.value.npcActivity) {
|
||||
gameStore.player.npcActivityNotifications = []
|
||||
}
|
||||
if (clearOptions.value.giftNotifications) {
|
||||
gameStore.player.giftNotifications = []
|
||||
}
|
||||
if (clearOptions.value.giftRejected) {
|
||||
gameStore.player.giftRejectedNotifications = []
|
||||
}
|
||||
|
||||
// 重置选项
|
||||
clearOptions.value = {
|
||||
battles: false,
|
||||
spyReports: false,
|
||||
spiedNotifications: false,
|
||||
missionReports: false,
|
||||
npcActivity: false,
|
||||
giftNotifications: false,
|
||||
giftRejected: false
|
||||
}
|
||||
|
||||
// 关闭popover
|
||||
showClearPopover.value = false
|
||||
|
||||
// 显示成功提示
|
||||
toast.success(t('messagesView.clearSuccess'))
|
||||
}
|
||||
|
||||
// 分页状态
|
||||
const ITEMS_PER_PAGE = 10
|
||||
const currentPage = ref({
|
||||
battles: 1,
|
||||
spy: 1,
|
||||
missions: 1,
|
||||
npc: 1
|
||||
})
|
||||
|
||||
// 对话框状态
|
||||
const showBattleDialog = ref(false)
|
||||
const showSpyDialog = ref(false)
|
||||
@@ -587,40 +772,107 @@
|
||||
const selectedMissionReport = ref<MissionReport | null>(null)
|
||||
const selectedNPCActivityNotification = ref<NPCActivityNotification | null>(null)
|
||||
|
||||
// 排序后的战斗报告(最新的在前)
|
||||
const sortedBattleReports = computed(() => {
|
||||
// 排序后的战斗报告(最新的在前)- 全部数据
|
||||
const allBattleReports = computed(() => {
|
||||
return [...gameStore.player.battleReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的间谍报告(最新的在前)
|
||||
const sortedSpyReports = computed(() => {
|
||||
// 排序后的间谍报告(最新的在前)- 全部数据
|
||||
const allSpyReports = computed(() => {
|
||||
return [...gameStore.player.spyReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的被侦查通知(最新的在前)
|
||||
const sortedSpiedNotifications = computed(() => {
|
||||
// 排序后的被侦查通知(最新的在前)- 全部数据
|
||||
const allSpiedNotifications = computed(() => {
|
||||
if (!gameStore.player.spiedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的任务报告(最新的在前)
|
||||
const sortedMissionReports = computed(() => {
|
||||
// 排序后的任务报告(最新的在前)- 全部数据
|
||||
const allMissionReports = computed(() => {
|
||||
if (!gameStore.player.missionReports) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.missionReports].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的NPC活动通知(最新的在前)
|
||||
const sortedNPCActivityNotifications = computed(() => {
|
||||
// 排序后的NPC活动通知(最新的在前)- 全部数据
|
||||
const allNPCActivityNotifications = computed(() => {
|
||||
if (!gameStore.player.npcActivityNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.npcActivityNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 战斗报告分页
|
||||
const battleReportsTotalPages = computed(() => Math.ceil(allBattleReports.value.length / ITEMS_PER_PAGE))
|
||||
const sortedBattleReports = computed(() => {
|
||||
const start = (currentPage.value.battles - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allBattleReports.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 侦查标签页合并数据(侦查报告 + 被侦查通知)
|
||||
const allSpyTabItems = computed(() => {
|
||||
const spyReports = allSpyReports.value.map(item => ({ ...item, type: 'spy' as const }))
|
||||
const spiedNotifications = allSpiedNotifications.value.map(item => ({ ...item, type: 'spied' as const }))
|
||||
return [...spyReports, ...spiedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
const spyTabTotalPages = computed(() => Math.ceil(allSpyTabItems.value.length / ITEMS_PER_PAGE))
|
||||
const paginatedSpyTabItems = computed(() => {
|
||||
const start = (currentPage.value.spy - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allSpyTabItems.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 从分页后的混合数据中分离出侦查报告和被侦查通知
|
||||
const sortedSpyReports = computed(() => {
|
||||
return paginatedSpyTabItems.value.filter(item => item.type === 'spy')
|
||||
})
|
||||
|
||||
const sortedSpiedNotifications = computed(() => {
|
||||
return paginatedSpyTabItems.value.filter(item => item.type === 'spied')
|
||||
})
|
||||
|
||||
// 任务报告分页
|
||||
const missionReportsTotalPages = computed(() => Math.ceil(allMissionReports.value.length / ITEMS_PER_PAGE))
|
||||
const sortedMissionReports = computed(() => {
|
||||
const start = (currentPage.value.missions - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allMissionReports.value.slice(start, end)
|
||||
})
|
||||
|
||||
// NPC标签页合并数据(活动通知 + 礼物通知 + 礼物被拒绝通知)
|
||||
const allNPCTabItems = computed(() => {
|
||||
const activities = allNPCActivityNotifications.value.map(item => ({ ...item, type: 'activity' as const }))
|
||||
const gifts = allGiftNotifications.value.map(item => ({ ...item, type: 'gift' as const }))
|
||||
const rejections = allGiftRejectedNotifications.value.map(item => ({ ...item, type: 'rejection' as const }))
|
||||
return [...activities, ...gifts, ...rejections].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
const npcTabTotalPages = computed(() => Math.ceil(allNPCTabItems.value.length / ITEMS_PER_PAGE))
|
||||
const paginatedNPCTabItems = computed(() => {
|
||||
const start = (currentPage.value.npc - 1) * ITEMS_PER_PAGE
|
||||
const end = start + ITEMS_PER_PAGE
|
||||
return allNPCTabItems.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 从分页后的混合数据中分离出各种NPC消息
|
||||
const sortedNPCActivityNotifications = computed(() => {
|
||||
return paginatedNPCTabItems.value.filter(item => item.type === 'activity')
|
||||
})
|
||||
|
||||
const sortedGiftNotifications = computed(() => {
|
||||
return paginatedNPCTabItems.value.filter(item => item.type === 'gift')
|
||||
})
|
||||
|
||||
const sortedGiftRejectedNotifications = computed(() => {
|
||||
return paginatedNPCTabItems.value.filter(item => item.type === 'rejection')
|
||||
})
|
||||
|
||||
// 未读战斗报告数量
|
||||
const unreadBattles = computed(() => {
|
||||
return gameStore.player.battleReports.filter(r => !r.read).length
|
||||
@@ -709,22 +961,41 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 排序后的礼物通知(最新的在前)
|
||||
const sortedGiftNotifications = computed(() => {
|
||||
// 礼物通知和被拒绝通知的全部数据(用于NPC标签页合并)
|
||||
const allGiftNotifications = computed(() => {
|
||||
if (!gameStore.player.giftNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 排序后的礼物被拒绝通知(最新的在前)
|
||||
const sortedGiftRejectedNotifications = computed(() => {
|
||||
const allGiftRejectedNotifications = computed(() => {
|
||||
if (!gameStore.player.giftRejectedNotifications) {
|
||||
return []
|
||||
}
|
||||
return [...gameStore.player.giftRejectedNotifications].sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
type PageKey = 'battles' | 'spy' | 'missions' | 'npc'
|
||||
const paginationConfigs = computed(() => ({
|
||||
battles: {
|
||||
totalPages: battleReportsTotalPages.value
|
||||
},
|
||||
spy: {
|
||||
totalPages: spyTabTotalPages.value
|
||||
},
|
||||
missions: {
|
||||
totalPages: missionReportsTotalPages.value
|
||||
},
|
||||
npc: {
|
||||
totalPages: npcTabTotalPages.value
|
||||
}
|
||||
}))
|
||||
|
||||
// 获取指定标签页的分页配置
|
||||
const getPaginationConfig = (key: PageKey) => paginationConfigs.value[key]
|
||||
|
||||
// 判断战斗结果Badge颜色
|
||||
const getBattleResultVariant = (report: BattleResult): 'default' | 'destructive' | 'secondary' => {
|
||||
if (report.winner === 'draw') {
|
||||
@@ -765,17 +1036,19 @@
|
||||
const openSpyReport = (report: SpyReport) => {
|
||||
selectedSpyReport.value = report
|
||||
showSpyDialog.value = true
|
||||
// 标记为已读
|
||||
if (!report.read) {
|
||||
report.read = true
|
||||
// 找到原始间谍报告对象并标记为已读(因为sortedSpyReports是副本)
|
||||
const originalReport = gameStore.player.spyReports.find(r => r.id === report.id)
|
||||
if (originalReport && !originalReport.read) {
|
||||
originalReport.read = true
|
||||
}
|
||||
}
|
||||
|
||||
// 打开被侦查通知
|
||||
const openSpiedNotification = (notification: SpiedNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
// 找到原始通知对象并标记为已读(因为sortedSpiedNotifications是副本)
|
||||
const originalNotification = gameStore.player.spiedNotifications?.find(n => n.id === notification.id)
|
||||
if (originalNotification && !originalNotification.read) {
|
||||
originalNotification.read = true
|
||||
}
|
||||
// 设置选中的通知并打开详情对话框
|
||||
selectedSpiedNotification.value = notification
|
||||
@@ -811,9 +1084,10 @@
|
||||
|
||||
// 打开NPC活动通知
|
||||
const openNPCActivityNotification = (notification: NPCActivityNotification) => {
|
||||
// 标记为已读
|
||||
if (!notification.read) {
|
||||
notification.read = true
|
||||
// 找到原始通知对象并标记为已读(因为sortedNPCActivityNotifications是副本)
|
||||
const originalNotification = gameStore.player.npcActivityNotifications?.find(n => n.id === notification.id)
|
||||
if (originalNotification && !originalNotification.read) {
|
||||
originalNotification.read = true
|
||||
}
|
||||
// 设置选中的通知并打开详情对话框
|
||||
selectedNPCActivityNotification.value = notification
|
||||
@@ -869,8 +1143,10 @@
|
||||
|
||||
// 标记礼物通知为已读
|
||||
const markGiftAsRead = (gift: GiftNotification) => {
|
||||
if (!gift.read) {
|
||||
gift.read = true
|
||||
// 找到原始礼物通知对象并标记为已读(因为gifts是副本)
|
||||
const originalGift = gameStore.player.giftNotifications?.find(g => g.id === gift.id)
|
||||
if (originalGift && !originalGift.read) {
|
||||
originalGift.read = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,8 +1179,10 @@
|
||||
|
||||
// 标记礼物被拒绝通知为已读
|
||||
const markGiftRejectedAsRead = (rejection: GiftRejectedNotification) => {
|
||||
if (!rejection.read) {
|
||||
rejection.read = true
|
||||
// 找到原始拒绝通知对象并标记为已读(因为rejections是副本)
|
||||
const originalRejection = gameStore.player.giftRejectedNotifications?.find(r => r.id === rejection.id)
|
||||
if (originalRejection && !originalRejection.read) {
|
||||
originalRejection.read = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
<h3 class="font-medium">{{ t('settings.gameSpeed') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.gameSpeedDesc') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2 flex-1 sm:flex-initial">
|
||||
<div class="flex items-center gap-2 sm:gap-4 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2 flex-1 sm:flex-initial">
|
||||
<Button @click="decreaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed <= 0.5">-</Button>
|
||||
<span class="min-w-[60px] text-center font-medium">{{ gameStore.gameSpeed }}x</span>
|
||||
<Button @click="increaseSpeed" variant="outline" size="sm" :disabled="gameStore.gameSpeed >= 10">+</Button>
|
||||
@@ -107,9 +107,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 页面聚焦时不发送 -->
|
||||
<div class="flex items-center justify-between pl-4 border-l-2" :class="{ 'opacity-50 pointer-events-none': !gameStore.notificationSettings?.browser }">
|
||||
<div
|
||||
class="flex items-center justify-between pl-4 border-l-2"
|
||||
:class="{ 'opacity-50 pointer-events-none': !gameStore.notificationSettings?.browser }"
|
||||
>
|
||||
<Label class="font-normal">{{ t('settings.suppressInFocus') }}</Label>
|
||||
<Switch
|
||||
<Switch
|
||||
:checked="gameStore.notificationSettings?.suppressInFocus"
|
||||
@update:checked="updateSuppressSetting"
|
||||
:disabled="!gameStore.notificationSettings?.browser"
|
||||
@@ -123,26 +126,29 @@
|
||||
<h3 class="font-medium">{{ t('settings.inAppNotifications') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.inAppNotificationsDesc') || t('settings.inAppNotifications') }}</p>
|
||||
</div>
|
||||
<Switch
|
||||
:checked="gameStore.notificationSettings?.inApp"
|
||||
@update:checked="(val: boolean) => updateInAppSetting(val)"
|
||||
/>
|
||||
<Switch :checked="gameStore.notificationSettings?.inApp" @update:checked="(val: boolean) => updateInAppSetting(val)" />
|
||||
</div>
|
||||
|
||||
<!-- 具体通知类型 -->
|
||||
<div class="border rounded-lg overflow-hidden" :class="{ 'opacity-50 pointer-events-none': areMainSwitchesOff }">
|
||||
<div
|
||||
<div
|
||||
class="flex items-center justify-between p-4 bg-muted/50 cursor-pointer select-none"
|
||||
@click="!areMainSwitchesOff && (isTypesExpanded = !isTypesExpanded)"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.notificationTypes') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ areMainSwitchesOff ? t('settings.notificationsDisabled') : (isTypesExpanded ? t('settings.collapseTypes') : t('settings.expandTypes')) }}
|
||||
{{
|
||||
areMainSwitchesOff
|
||||
? t('settings.notificationsDisabled')
|
||||
: isTypesExpanded
|
||||
? t('settings.collapseTypes')
|
||||
: t('settings.expandTypes')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" class="h-8 w-8 p-0">
|
||||
<component :is="isTypesExpanded ? ChevronUp : ChevronDown" class="h-4 w-4" />
|
||||
<component :is="isTypesExpanded ? ChevronUp : ChevronDown" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -157,8 +163,8 @@
|
||||
</div>
|
||||
<!-- 研究完成 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<Label class="font-normal cursor-pointer" @click="toggleType('research')">{{ t('settings.researchComplete') }}</Label>
|
||||
<Switch
|
||||
<Label class="font-normal cursor-pointer" @click="toggleType('research')">{{ t('settings.researchComplete') }}</Label>
|
||||
<Switch
|
||||
:checked="gameStore.notificationSettings?.types.research"
|
||||
@update:checked="(val: boolean) => updateTypeSetting('research', val)"
|
||||
/>
|
||||
@@ -168,6 +174,54 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 帮助提示设置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('hints.hintsEnabled') }}</CardTitle>
|
||||
<CardDescription>{{ t('hints.hintsEnabledDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 启用/禁用提示 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('hints.hintsEnabled') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('hints.hintsEnabledDesc') }}</p>
|
||||
</div>
|
||||
<Switch :checked="hintsEnabled" @update:checked="setHintsEnabled" />
|
||||
</div>
|
||||
|
||||
<!-- 重置提示 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('hints.resetHints') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('hints.resetHintsDesc') }}</p>
|
||||
</div>
|
||||
<Button @click="handleResetHints" variant="outline">
|
||||
<RotateCcw class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 显示设置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ t('settings.displaySettings') }}</CardTitle>
|
||||
<CardDescription>{{ t('settings.displaySettingsDesc') }}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 背景动画 -->
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-medium">{{ t('settings.backgroundAnimation') }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ t('settings.backgroundAnimationDesc') }}</p>
|
||||
</div>
|
||||
<Switch :checked="gameStore.player.backgroundEnabled ?? false" @update:checked="updateBackgroundSetting" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 关于 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -198,6 +252,11 @@
|
||||
<div class="pt-2 border-t space-y-2">
|
||||
<h3 class="text-sm font-medium">{{ t('settings.community') }}</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Privacy Policy -->
|
||||
<Button variant="outline" class="w-full justify-start" @click="openPrivacy">
|
||||
<Shield class="mr-2 h-4 w-4" />
|
||||
{{ t('settings.privacyPolicy') }}
|
||||
</Button>
|
||||
<!-- GitHub -->
|
||||
<Button variant="outline" class="w-full justify-start" @click="openGithub">
|
||||
<ExternalLink class="mr-2 h-4 w-4" />
|
||||
@@ -234,6 +293,9 @@
|
||||
|
||||
<!-- 更新对话框 -->
|
||||
<UpdateDialog v-model:open="showUpdateDialog" :version-info="updateInfo" />
|
||||
|
||||
<!-- 隐私协议弹窗 -->
|
||||
<PrivacyDialog v-model:open="showPrivacyDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -255,15 +317,31 @@
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw, ChevronDown, ChevronUp } from 'lucide-vue-next'
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
MessagesSquare,
|
||||
Play,
|
||||
Pause,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
RotateCcw,
|
||||
Shield
|
||||
} from 'lucide-vue-next'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { toast } from 'vue-sonner'
|
||||
import pkg from '../../package.json'
|
||||
import { checkLatestVersion, canCheckVersion } from '@/utils/versionCheck'
|
||||
import type { VersionInfo } from '@/utils/versionCheck'
|
||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||
import PrivacyDialog from '@/components/PrivacyDialog.vue'
|
||||
import { useHints } from '@/composables/useHints'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { hintsEnabled, setHintsEnabled, resetHints } = useHints()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
@@ -278,7 +356,7 @@
|
||||
|
||||
const isTypesExpanded = ref(false)
|
||||
|
||||
// Ensure notification settings exist
|
||||
// 确保通知设置存在
|
||||
if (!gameStore.notificationSettings) {
|
||||
gameStore.notificationSettings = {
|
||||
browser: false,
|
||||
@@ -293,7 +371,7 @@
|
||||
return !s?.browser && !s?.inApp
|
||||
})
|
||||
|
||||
// Auto-collapse if main switches are off
|
||||
// 当主开关关闭时自动折叠
|
||||
// watch(areMainSwitchesOff, (val) => {
|
||||
// if (val) isTypesExpanded.value = false
|
||||
// })
|
||||
@@ -310,13 +388,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const updateTypeSetting = (key: string, val: boolean) => {
|
||||
const updateTypeSetting = (key: 'construction' | 'research', val: boolean) => {
|
||||
if (gameStore.notificationSettings) {
|
||||
gameStore.notificationSettings.types[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
const toggleType = (key: string) => {
|
||||
const toggleType = (key: 'construction' | 'research') => {
|
||||
if (gameStore.notificationSettings) {
|
||||
const current = gameStore.notificationSettings.types[key]
|
||||
gameStore.notificationSettings.types[key] = !current
|
||||
@@ -385,6 +463,14 @@
|
||||
window.open(`https://qm.qq.com/q/${pkg.id}`, '_blank')
|
||||
}
|
||||
|
||||
// 隐私协议弹窗状态
|
||||
const showPrivacyDialog = ref(false)
|
||||
|
||||
// 打开隐私协议弹窗
|
||||
const openPrivacy = () => {
|
||||
showPrivacyDialog.value = true
|
||||
}
|
||||
|
||||
// 手动检查版本
|
||||
const showUpdateDialog = ref(false)
|
||||
const updateInfo = ref<VersionInfo | null>(null)
|
||||
@@ -514,6 +600,7 @@
|
||||
|
||||
// 清除数据
|
||||
const handleClearData = () => {
|
||||
gameStore.isPaused = true
|
||||
confirmTitle.value = t('settings.clearConfirmTitle')
|
||||
confirmMessage.value = t('settings.clearConfirmMessage')
|
||||
showConfirmDialog.value = true
|
||||
@@ -583,4 +670,15 @@
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 重置提示
|
||||
const handleResetHints = () => {
|
||||
resetHints()
|
||||
toast.success(t('hints.resetHints'))
|
||||
}
|
||||
|
||||
// 更新背景设置
|
||||
const updateBackgroundSetting = (val: boolean) => {
|
||||
gameStore.player.backgroundEnabled = val
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user