test(cache): 修复CacheConfigTest边界值测试

- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl
- 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE
- 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查
- 所有1266个测试用例通过
- 覆盖率: 指令81.89%, 行88.48%, 分支51.55%

docs: 添加项目状态报告
- 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态
- 包含质量指标、已完成功能、待办事项和技术债务
This commit is contained in:
Your Name
2026-03-02 13:31:54 +08:00
parent 32d6449ea4
commit 91a0b77f7a
2272 changed files with 221995 additions and 503 deletions

View File

@@ -0,0 +1,84 @@
<template>
<div class="space-y-3">
<div class="text-sm font-semibold text-mosquito-ink">{{ title }}</div>
<div class="space-y-2">
<label
v-for="field in fields"
:key="field.key"
class="flex items-center gap-2 text-xs text-mosquito-ink/80"
>
<input
type="checkbox"
class="h-4 w-4"
:checked="isChecked(field.key)"
:disabled="field.required"
@change="onToggle(field.key, ($event.target as HTMLInputElement).checked)"
/>
<span>{{ field.label }}</span>
<span v-if="field.required" class="rounded-full bg-mosquito-accent/10 px-2 py-0.5 text-[10px] font-semibold text-mosquito-brand">
必选
</span>
</label>
</div>
<div class="flex items-center justify-between text-xs text-mosquito-ink/70">
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="selectAll">全选</button>
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="clearOptional">仅保留必选</button>
<button
data-test="export-button"
class="mos-btn mos-btn-accent !py-1 !px-3 !text-xs"
@click="emit('export')"
>
导出
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
export type ExportField = {
key: string
label: string
required?: boolean
}
const props = defineProps<{
title: string
fields: ExportField[]
selected: string[]
}>()
const emit = defineEmits<{
(event: 'update:selected', value: string[]): void
(event: 'export'): void
}>()
const requiredKeys = computed(() => props.fields.filter((field) => field.required).map((field) => field.key))
const normalizeSelection = (next: string[]) => {
const merged = new Set([...requiredKeys.value, ...next])
return props.fields.map((field) => field.key).filter((key) => merged.has(key))
}
const isChecked = (key: string) => normalizeSelection(props.selected).includes(key)
const onToggle = (key: string, checked: boolean) => {
if (requiredKeys.value.includes(key)) {
emit('update:selected', normalizeSelection(props.selected))
return
}
const next = checked
? [...props.selected, key]
: props.selected.filter((item) => item !== key)
emit('update:selected', normalizeSelection(next))
}
const selectAll = () => {
emit('update:selected', normalizeSelection(props.fields.map((field) => field.key)))
}
const clearOptional = () => {
emit('update:selected', normalizeSelection([]))
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="space-y-3">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap items-center gap-2 text-xs text-mosquito-ink/70">
<slot name="filters" />
</div>
<div class="flex items-center gap-2 text-xs text-mosquito-ink/70">
<slot name="actions" />
</div>
</div>
<slot />
<div v-if="showPagination" class="mt-2 flex items-center justify-between text-xs text-mosquito-ink/70">
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" :disabled="page <= 0" @click="$emit('prev')">
上一页
</button>
<div> {{ page + 1 }} / {{ totalPages }} </div>
<button
class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs"
:disabled="page >= totalPages - 1"
@click="$emit('next')"
>
下一页
</button>
</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
page?: number
totalPages?: number
}>(),
{
page: 0,
totalPages: 0
}
)
const showPagination = props.totalPages > 1
defineEmits<{
prev: []
next: []
}>()
</script>

View File

@@ -0,0 +1,56 @@
<template>
<section class="space-y-6">
<header v-if="$slots.title || $slots.subtitle" class="space-y-2">
<h1 v-if="$slots.title" class="mos-title text-2xl font-semibold">
<slot name="title" />
</h1>
<p v-if="$slots.subtitle" class="mos-muted text-sm">
<slot name="subtitle" />
</p>
</header>
<div class="mos-card p-5">
<FilterPaginationBar
v-if="page !== undefined && totalPages !== undefined"
:page="page"
:total-pages="totalPages"
@prev="emit('prev')"
@next="emit('next')"
>
<template #filters>
<slot name="filters" />
</template>
<template #actions>
<slot name="actions" />
</template>
<slot />
<slot name="empty" />
</FilterPaginationBar>
<template v-else>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap items-center gap-2">
<slot name="filters" />
</div>
<div class="flex flex-wrap items-center gap-2">
<slot name="actions" />
</div>
</div>
<div class="mt-4 space-y-3">
<slot />
<slot name="empty" />
</div>
</template>
<div v-if="$slots.footer" class="mt-4">
<slot name="footer" />
</div>
</div>
</section>
</template>
<script setup lang="ts">
import FilterPaginationBar from './FilterPaginationBar.vue'
defineProps<{ page?: number; totalPages?: number }>()
const emit = defineEmits<{ (event: 'prev'): void; (event: 'next'): void }>()
</script>

View File

@@ -0,0 +1,40 @@
import { mount } from '@vue/test-utils'
import ExportFieldPanel from '../ExportFieldPanel.vue'
describe('ExportFieldPanel', () => {
it('emits updated selection when toggling optional field', async () => {
const wrapper = mount(ExportFieldPanel, {
props: {
title: 'Fields',
fields: [
{ key: 'name', label: 'Name', required: true },
{ key: 'status', label: 'Status' }
],
selected: ['name']
}
})
const inputs = wrapper.findAll('input[type="checkbox"]')
expect(inputs).toHaveLength(2)
expect((inputs[0].element as HTMLInputElement).checked).toBe(true)
expect((inputs[0].element as HTMLInputElement).disabled).toBe(true)
await inputs[1].setValue(true)
const emitted = wrapper.emitted('update:selected')
expect(emitted).toBeTruthy()
expect(emitted?.[0][0]).toEqual(['name', 'status'])
})
it('emits export event when clicking export button', async () => {
const wrapper = mount(ExportFieldPanel, {
props: {
title: 'Fields',
fields: [{ key: 'name', label: 'Name' }],
selected: ['name']
}
})
await wrapper.get('[data-test="export-button"]').trigger('click')
expect(wrapper.emitted('export')).toBeTruthy()
})
})

View File

@@ -0,0 +1,26 @@
import { mount } from '@vue/test-utils'
import ListSection from '../ListSection.vue'
describe('ListSection', () => {
it('renders provided slots', () => {
const wrapper = mount(ListSection, {
slots: {
title: '<div data-test="title">Title</div>',
subtitle: '<div data-test="subtitle">Subtitle</div>',
filters: '<div data-test="filters">Filters</div>',
actions: '<div data-test="actions">Actions</div>',
default: '<div data-test="content">Content</div>',
empty: '<div data-test="empty">Empty</div>',
footer: '<div data-test="footer">Footer</div>'
}
})
expect(wrapper.find('[data-test="title"]').text()).toBe('Title')
expect(wrapper.find('[data-test="subtitle"]').text()).toBe('Subtitle')
expect(wrapper.find('[data-test="filters"]').text()).toBe('Filters')
expect(wrapper.find('[data-test="actions"]').text()).toBe('Actions')
expect(wrapper.find('[data-test="content"]').text()).toBe('Content')
expect(wrapper.find('[data-test="empty"]').text()).toBe('Empty')
expect(wrapper.find('[data-test="footer"]').text()).toBe('Footer')
})
})