refactor: 优化主界面布局与通知系统

重构App.vue,首页独立无侧边栏,其他页面采用统一侧边栏布局。新增右下角固定通知区,集成返回顶部、队列通知、外交通知和敌方警报。移除新手引导组件,替换为弱引导提示系统。支持星球重命名弹窗。优化NPC成长与行为定时器逻辑,提升性能和可维护性。删除issue模板及相关文档描述。
This commit is contained in:
谦君
2025-12-19 12:01:45 +08:00
parent a689ce21b7
commit 752cade67c
61 changed files with 5774 additions and 2817 deletions

View File

@@ -6,7 +6,7 @@
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-2xl',
props.class
)
"

View File

@@ -4,7 +4,7 @@
v-bind="delegatedProps"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[60] bg-black/80',
props.class
)
"

View File

@@ -6,7 +6,7 @@
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[60] w-[calc(100vw-3rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:w-auto sm:min-w-[764px] flex flex-col p-0',
containerClass
)
"

View File

@@ -0,0 +1,93 @@
<template>
<div v-if="totalPages > 1" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40">
<div class="flex items-center gap-2">
<!-- 上一页按钮 - 圆形胶囊 -->
<button
v-if="currentPage > 1"
@click="emit('update:page', currentPage - 1)"
class="h-10 w-10 rounded-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg flex items-center justify-center hover:bg-accent transition-colors"
>
<ChevronLeft class="h-5 w-5" />
</button>
<!-- 页码 - 椭圆形胶囊 -->
<div class="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border rounded-full py-2 px-3 shadow-lg flex items-center gap-1">
<button
v-for="pageNum in pageNumbers"
:key="pageNum"
@click="emit('update:page', pageNum)"
class="h-8 min-w-8 px-2 rounded-full text-sm font-medium transition-colors"
:class="
pageNum === currentPage
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent'
"
>
{{ pageNum }}
</button>
</div>
<!-- 下一页按钮 - 圆形胶囊 -->
<button
v-if="currentPage < totalPages"
@click="emit('update:page', currentPage + 1)"
class="h-10 w-10 rounded-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border shadow-lg flex items-center justify-center hover:bg-accent transition-colors"
>
<ChevronRight class="h-5 w-5" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
interface Props {
page: number
totalPages: number
maxVisible?: number
}
const props = withDefaults(defineProps<Props>(), {
maxVisible: 3
})
const emit = defineEmits<{
'update:page': [page: number]
}>()
const currentPage = computed(() => props.page)
// 生成页码列表 - 最多显示指定数量页码,不含省略号
const pageNumbers = computed(() => {
const pages: number[] = []
const { totalPages, maxVisible } = props
const current = currentPage.value
if (totalPages <= maxVisible) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i)
}
} else {
let start = current - Math.floor(maxVisible / 2)
let end = current + Math.floor(maxVisible / 2)
// 边界调整
if (start < 1) {
start = 1
end = maxVisible
}
if (end > totalPages) {
end = totalPages
start = totalPages - maxVisible + 1
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
}
return pages
})
</script>

View File

@@ -6,3 +6,4 @@ export { default as PaginationItem } from './PaginationItem.vue'
export { default as PaginationLast } from './PaginationLast.vue'
export { default as PaginationNext } from './PaginationNext.vue'
export { default as PaginationPrevious } from './PaginationPrevious.vue'
export { default as FixedPagination } from './FixedPagination.vue'

View File

@@ -86,7 +86,6 @@
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
import { useTutorial } from '@/composables/useTutorial'
import { useRouter } from 'vue-router'
defineOptions({
@@ -101,47 +100,17 @@
const router = useRouter()
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
const { tutorialState, currentStep, nextStep } = useTutorial()
// 包装setOpenMobile以拦截教程期间的关闭操作
// 处理移动端侧边栏打开/关闭
const handleOpenMobileChange = (open: boolean) => {
// 如果是移动端且在教程的菜单相关步骤,阻止关闭侧边栏
if (tutorialState.value.isActive && currentStep.value) {
// 只在第3步期间阻止关闭侧边栏让玩家必须手动打开
if (currentStep.value.id === 'menu_intro_mobile') {
// 只允许打开,不允许关闭
if (open) {
setOpenMobile(true)
}
// 如果试图关闭,忽略该操作,保持打开状态
return
}
}
// 其他情况正常更新
setOpenMobile(open)
}
// 监听openMobile变化在移动端教程第3步时侧边栏打开后自动推进到第4步
watch(
() => openMobile.value,
(isOpen) => {
if (isMobile.value && tutorialState.value.isActive && currentStep.value) {
// 如果在第3步且侧边栏刚打开,自动推进到第4步
if (currentStep.value.id === 'menu_intro_mobile' && isOpen) {
setTimeout(() => {
nextStep()
}, 300) // 延迟300ms让侧边栏动画完成
}
}
}
)
// 监听路由变化,在移动端关闭侧边栏
watch(
() => router.currentRoute.value.path,
() => {
if (isMobile.value && openMobile.value) {
// 路由变化时关闭移动端侧边栏
setOpenMobile(false)
}
}

View File

@@ -16,18 +16,11 @@
<script setup lang="ts">
import type { HTMLAttributes, Ref } from 'vue'
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
import { useMediaQuery, useVModel } from '@vueuse/core'
import { TooltipProvider } from 'reka-ui'
import { computed, ref } from 'vue'
import { cn } from '@/lib/utils'
import {
provideSidebarContext,
SIDEBAR_COOKIE_MAX_AGE,
SIDEBAR_COOKIE_NAME,
SIDEBAR_KEYBOARD_SHORTCUT,
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_ICON
} from './utils'
import { provideSidebarContext, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './utils'
const props = withDefaults(
defineProps<{
@@ -36,7 +29,7 @@
class?: HTMLAttributes['class']
}>(),
{
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
defaultOpen: true,
open: undefined
}
)
@@ -54,30 +47,17 @@
}) as Ref<boolean>
const setOpen = (value: boolean) => {
open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
open.value = value
}
const setOpenMobile = (value: boolean) => {
openMobile.value = value
}
// Helper to toggle the sidebar.
const toggleSidebar = () => {
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
}
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
}
})
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = computed(() => (open.value ? 'expanded' : 'collapsed'))
provideSidebarContext({

View File

@@ -1,12 +1,9 @@
import type { ComputedRef, Ref } from 'vue'
import { createContext } from 'reka-ui'
export const SIDEBAR_COOKIE_NAME = 'sidebar_state'
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const SIDEBAR_WIDTH = '16rem'
export const SIDEBAR_WIDTH_MOBILE = '18rem'
export const SIDEBAR_WIDTH_ICON = '3rem'
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
export const [useSidebar, provideSidebarContext] = createContext<{
state: ComputedRef<'expanded' | 'collapsed'>