feat: 新增NPC与外交逻辑,优化UI组件结构

重构并精简了部分UI组件,移除冗余弹窗与详情组件,新增NPC相关逻辑(npcBehaviorLogic、npcGrowthLogic、npcStore等)及外交逻辑(diplomaticLogic、DiplomacyView)。完善分页、标签、复选框等通用UI组件。优化战报弹窗,调整README下载链接为相对路径,修复部分国际化内容。
This commit is contained in:
谦君
2025-12-15 08:23:45 +08:00
parent 44580909a3
commit 9b9fda0400
164 changed files with 18628 additions and 2775 deletions

View File

@@ -4,7 +4,7 @@ import { cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
'inline-flex items-center justify-center rounded-sm border h-5 min-w-5 px-1 text-xs font-medium tabular-nums select-none w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {

View File

@@ -5,20 +5,20 @@
</template>
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '.'
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button'
})
const props = withDefaults(defineProps<Props>(), {
as: 'button'
})
</script>

View File

@@ -0,0 +1,35 @@
<template>
<CheckboxRoot
v-slot="slotProps"
data-slot="checkbox"
v-bind="forwarded"
:class="
cn(
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"
>
<CheckboxIndicator data-slot="checkbox-indicator" class="grid place-content-center text-current transition-none">
<slot v-bind="slotProps">
<Check class="size-3.5" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Check } from 'lucide-vue-next'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

@@ -0,0 +1 @@
export { default as Checkbox } from './Checkbox.vue'

View File

@@ -0,0 +1,71 @@
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="scrollable-dialog-content"
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 flex flex-col p-0',
containerClass
)
"
>
<!-- 固定的头部 -->
<div class="shrink-0 px-4 pt-4 pb-3 sm:px-6 sm:pt-6 sm:pb-4 border-b">
<slot name="header" />
</div>
<!-- 可滚动的内容区域 -->
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4">
<slot />
</div>
<!-- 可选的固定底部 -->
<div v-if="$slots.footer" class="shrink-0 px-4 pb-4 pt-3 sm:px-6 sm:pb-6 sm:pt-4 border-t">
<slot name="footer" />
</div>
<!-- 关闭按钮 -->
<DialogClose
v-if="showCloseButton"
data-slot="dialog-close"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 z-10"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { X } from 'lucide-vue-next'
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import DialogOverlay from './DialogOverlay.vue'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(
defineProps<
DialogContentProps & {
containerClass?: HTMLAttributes['class']
showCloseButton?: boolean
}
>(),
{
showCloseButton: true
}
)
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'containerClass')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

@@ -6,5 +6,6 @@ export { default as DialogFooter } from './DialogFooter.vue'
export { default as DialogHeader } from './DialogHeader.vue'
export { default as DialogOverlay } from './DialogOverlay.vue'
export { default as DialogScrollContent } from './DialogScrollContent.vue'
export { default as ScrollableDialogContent } from './ScrollableDialogContent.vue'
export { default as DialogTitle } from './DialogTitle.vue'
export { default as DialogTrigger } from './DialogTrigger.vue'

View File

@@ -0,0 +1,28 @@
<template>
<PaginationRoot
v-slot="slotProps"
data-slot="pagination"
v-bind="forwarded"
:class="cn('mx-auto flex w-full justify-center', props.class)"
>
<slot v-bind="slotProps" />
</PaginationRoot>
</template>
<script setup lang="ts">
import type { PaginationRootEmits, PaginationRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { PaginationRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<
PaginationRootProps & {
class?: HTMLAttributes['class']
}
>()
const emits = defineEmits<PaginationRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

@@ -0,0 +1,22 @@
<template>
<PaginationList
v-slot="slotProps"
data-slot="pagination-content"
v-bind="delegatedProps"
:class="cn('flex flex-row items-center gap-1', props.class)"
>
<slot v-bind="slotProps" />
</PaginationList>
</template>
<script setup lang="ts">
import type { PaginationListProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { PaginationList } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<PaginationListProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,25 @@
<template>
<PaginationEllipsis
data-slot="pagination-ellipsis"
v-bind="delegatedProps"
:class="cn('flex size-9 items-center justify-center', props.class)"
>
<slot>
<MoreHorizontal class="size-4" />
<span class="sr-only">More pages</span>
</slot>
</PaginationEllipsis>
</template>
<script setup lang="ts">
import type { PaginationEllipsisProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { MoreHorizontal } from 'lucide-vue-next'
import { PaginationEllipsis } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<PaginationEllipsisProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,38 @@
<template>
<PaginationFirst
data-slot="pagination-first"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded"
>
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">First</span>
</slot>
</PaginationFirst>
</template>
<script setup lang="ts">
import type { PaginationFirstProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
import { reactiveOmit } from '@vueuse/core'
import { ChevronLeftIcon } from 'lucide-vue-next'
import { PaginationFirst, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = withDefaults(
defineProps<
PaginationFirstProps & {
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
>(),
{
size: 'default'
}
)
const delegatedProps = reactiveOmit(props, 'class', 'size')
const forwarded = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,42 @@
<template>
<PaginationListItem
data-slot="pagination-item"
v-bind="delegatedProps"
:class="
cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size
}),
props.class
)
"
>
<slot />
</PaginationListItem>
</template>
<script setup lang="ts">
import type { PaginationListItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
import { reactiveOmit } from '@vueuse/core'
import { PaginationListItem } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = withDefaults(
defineProps<
PaginationListItemProps & {
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
isActive?: boolean
}
>(),
{
size: 'icon'
}
)
const delegatedProps = reactiveOmit(props, 'class', 'size', 'isActive')
</script>

View File

@@ -0,0 +1,38 @@
<template>
<PaginationLast
data-slot="pagination-last"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded"
>
<slot>
<span class="hidden sm:block">Last</span>
<ChevronRightIcon />
</slot>
</PaginationLast>
</template>
<script setup lang="ts">
import type { PaginationLastProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
import { reactiveOmit } from '@vueuse/core'
import { ChevronRightIcon } from 'lucide-vue-next'
import { PaginationLast, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = withDefaults(
defineProps<
PaginationLastProps & {
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
>(),
{
size: 'default'
}
)
const delegatedProps = reactiveOmit(props, 'class', 'size')
const forwarded = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,38 @@
<template>
<PaginationNext
data-slot="pagination-next"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded"
>
<slot>
<span class="hidden sm:block">Next</span>
<ChevronRightIcon />
</slot>
</PaginationNext>
</template>
<script setup lang="ts">
import type { PaginationNextProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
import { reactiveOmit } from '@vueuse/core'
import { ChevronRightIcon } from 'lucide-vue-next'
import { PaginationNext, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = withDefaults(
defineProps<
PaginationNextProps & {
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
>(),
{
size: 'default'
}
)
const delegatedProps = reactiveOmit(props, 'class', 'size')
const forwarded = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,38 @@
<template>
<PaginationPrev
data-slot="pagination-previous"
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
v-bind="forwarded"
>
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">Previous</span>
</slot>
</PaginationPrev>
</template>
<script setup lang="ts">
import type { PaginationPrevProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
import { reactiveOmit } from '@vueuse/core'
import { ChevronLeftIcon } from 'lucide-vue-next'
import { PaginationPrev, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = withDefaults(
defineProps<
PaginationPrevProps & {
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
>(),
{
size: 'default'
}
)
const delegatedProps = reactiveOmit(props, 'class', 'size')
const forwarded = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,8 @@
export { default as Pagination } from './Pagination.vue'
export { default as PaginationContent } from './PaginationContent.vue'
export { default as PaginationEllipsis } from './PaginationEllipsis.vue'
export { default as PaginationFirst } from './PaginationFirst.vue'
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'

View File

@@ -4,12 +4,13 @@
data-sidebar="menu-badge"
:class="
cn(
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
'text-sidebar-foreground pointer-events-none flex h-5 min-w-5 items-center justify-center rounded-sm px-1 text-xs font-medium tabular-nums select-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'absolute right-1',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
'group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:right-0 group-data-[collapsible=icon]:-top-1 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4',
props.class
)
"

View File

@@ -53,19 +53,19 @@
passive: (props.open === undefined) as false
}) as Ref<boolean>
function setOpen(value: 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}`
}
function setOpenMobile(value: boolean) {
const setOpenMobile = (value: boolean) => {
openMobile.value = value
}
// Helper to toggle the sidebar.
function toggleSidebar() {
const toggleSidebar = () => {
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
}

View File

@@ -0,0 +1,19 @@
<template>
<TabsRoot v-slot="slotProps" data-slot="tabs" v-bind="forwarded" :class="cn('flex flex-col gap-2', props.class)">
<slot v-bind="slotProps" />
</TabsRoot>
</template>
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<TabsRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

@@ -0,0 +1,17 @@
<template>
<TabsContentRoot data-slot="tabs-content" :class="cn('flex-1 outline-none', props.class)" v-bind="delegatedProps">
<slot />
</TabsContentRoot>
</template>
<script setup lang="ts">
import type { TabsContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsContent as TabsContentRoot } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -0,0 +1,27 @@
<template>
<TabsListRoot
data-slot="tabs-list"
v-bind="delegatedProps"
:class="
cn(
'bg-muted text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-[3px]',
tabCount && tabCount > 3 ? (tabCount > 6 ? 'h-[85px] sm:h-9' : 'h-[65px] sm:h-9') : 'h-9',
props.class
)
"
>
<slot />
</TabsListRoot>
</template>
<script setup lang="ts">
import type { TabsListProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsList as TabsListRoot } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsListProps & { class?: HTMLAttributes['class']; tabCount?: number }>()
const delegatedProps = reactiveOmit(props, 'class', 'tabCount')
</script>

View File

@@ -0,0 +1,34 @@
<template>
<TabsTriggerRoot
data-slot="tabs-trigger"
:class="
cn(
'inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all',
'text-muted-foreground hover:text-foreground',
'data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-md data-[state=active]:border-border',
'dark:data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-border dark:data-[state=active]:shadow-lg',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:outline-1',
'disabled:pointer-events-none disabled:opacity-50',
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
"
v-bind="forwardedProps"
>
<slot />
</TabsTriggerRoot>
</template>
<script setup lang="ts">
import type { TabsTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TabsTrigger as TabsTriggerRoot, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>

View File

@@ -0,0 +1,4 @@
export { default as Tabs } from './Tabs.vue'
export { default as TabsContent } from './TabsContent.vue'
export { default as TabsList } from './TabsList.vue'
export { default as TabsTrigger } from './TabsTrigger.vue'