feat(management): useManagementFilters——URL query 真源/预设窗/渠道哨兵

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-10 10:02:35 +08:00
parent c56f13059d
commit df8d2b30d5
2 changed files with 197 additions and 0 deletions
@@ -0,0 +1,119 @@
// useManagementFilters 单测(vitest)。本文件不进 app 构建/类型检查
// (tsconfig.app.json exclude __tests__,vitest 自行解析类型),
// `npm run test` 执行。
//
// composable 依赖 vue-router 注入,仓库无先例;node 环境(无 DOM),
// 选用 createApp + app.use(router) + app.runWithContext() 提供真实
// route/router 注入,不 mock、不挂载组件。
import { describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import { presetRange, useManagementFilters } from '../useManagementFilters'
/** 建一个带 memory 路由的 filters 实例;query 为初始 URL query。 */
async function setup(query: Record<string, string> = {}) {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/', component: { render: () => null } }],
})
const app = createApp({ render: () => null })
app.use(router)
await router.push({ path: '/', query })
const filters = app.runWithContext(() => useManagementFilters())
return { router, filters }
}
const D = (ymdNum: number) => new Date(Math.floor(ymdNum / 10000), (Math.floor(ymdNum / 100) % 100) - 1, ymdNum % 100)
describe('presetRange(注入 today,确定性日期)', () => {
it('last7 → 含今天共 7 天(2026-09-10 → 20260904..20260910)', () => {
expect(presetRange('last7', D(20260910))).toEqual({ start: 20260904, end: 20260910 })
})
it('last30 → 30 天窗(2026-09-10 → 20260812..20260910)', () => {
expect(presetRange('last30', D(20260910))).toEqual({ start: 20260812, end: 20260910 })
})
it('thisMonth → 月初到今天(2026-09-10 → 20260901..20260910)', () => {
expect(presetRange('thisMonth', D(20260910))).toEqual({ start: 20260901, end: 20260910 })
})
it('lastMonth → 上月整月(2026-08-15 → 20260701..20260731,31 天月)', () => {
expect(presetRange('lastMonth', D(20260815))).toEqual({ start: 20260701, end: 20260731 })
})
it('lastMonth 30 天月(2026-07-15 → 20260601..20260630)', () => {
expect(presetRange('lastMonth', D(20260715))).toEqual({ start: 20260601, end: 20260630 })
})
it('lastMonth 跨年(2026-01-15 → 20251201..20251231)', () => {
expect(presetRange('lastMonth', D(20260115))).toEqual({ start: 20251201, end: 20251231 })
})
it('custom → 今天单日兜底(2026-09-10 → 20260910..20260910)', () => {
expect(presetRange('custom', D(20260910))).toEqual({ start: 20260910, end: 20260910 })
})
})
describe('useManagementFilters(URL query 为真源)', () => {
it('空 query → channel=all / start,end=last30 默认 / compare=previous', async () => {
const { filters } = await setup()
expect(filters.channel.value).toBe('all')
expect(filters.compare.value).toBe('previous')
// 默认值与 presetRange('last30')(真实今天)同源,同日执行必一致
expect(filters.start.value).toBe(presetRange('last30').start)
expect(filters.end.value).toBe(presetRange('last30').end)
})
it('合法 query → 原样读出(wechat / 日期 / team_median)', async () => {
const { filters } = await setup({
channel: 'wechat',
start: '20250101',
end: '20250131',
compare: 'team_median',
})
expect(filters.channel.value).toBe('wechat')
expect(filters.start.value).toBe(20250101)
expect(filters.end.value).toBe(20250131)
expect(filters.compare.value).toBe('team_median')
})
it('非法 query 值 → 回退默认(channel=xx / start=abc / end=19990101 早于下限 / compare=foo)', async () => {
const { filters } = await setup({ channel: 'xx', start: 'abc', end: '19990101', compare: 'foo' })
expect(filters.channel.value).toBe('all')
expect(filters.compare.value).toBe('previous')
expect(filters.start.value).toBe(presetRange('last30').start)
expect(filters.end.value).toBe(presetRange('last30').end)
})
it('apply({channel}) → query 更新且其余参数保留', async () => {
const { router, filters } = await setup({ compare: 'team_median' })
filters.apply({ channel: 'whatsapp' })
await vi.waitFor(() => expect(router.currentRoute.value.query.channel).toBe('whatsapp'))
expect(filters.channel.value).toBe('whatsapp')
// 未 patch 的 compare 原样保留
expect(router.currentRoute.value.query.compare).toBe('team_median')
expect(filters.compare.value).toBe('team_median')
})
it('applyPreset(预设) → 清掉 query 里的 start/end,默认值(仍为 last30)生效', async () => {
const { router, filters } = await setup({ start: '20250101', end: '20250131', channel: 'wechat' })
filters.applyPreset('last7')
await vi.waitFor(() => expect(router.currentRoute.value.query.start).toBeUndefined())
expect(router.currentRoute.value.query.end).toBeUndefined()
// channel 不在清理范围,保留
expect(filters.channel.value).toBe('wechat')
// 清掉后回退默认(last30,与真实今天同源)
expect(filters.start.value).toBe(presetRange('last30').start)
expect(filters.end.value).toBe(presetRange('last30').end)
})
it("applyPreset('custom', {...}) → query 带具体日期", async () => {
const { router, filters } = await setup()
filters.applyPreset('custom', { start: 20260101, end: 20260131 })
await vi.waitFor(() => expect(router.currentRoute.value.query.start).toBe('20260101'))
expect(router.currentRoute.value.query.end).toBe('20260131')
expect(filters.start.value).toBe(20260101)
expect(filters.end.value).toBe(20260131)
})
})
@@ -0,0 +1,78 @@
// 管理页统一筛选(URL query 为真源——刷新/分享保持)。批次C:驾驶舱用;
// 批次D 员工页复用(view 等额外参数由页面自行挂 query)。
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export type Preset = 'last7' | 'last30' | 'thisMonth' | 'lastMonth' | 'custom'
export const CHANNELS = [
{ value: 'all', label: '全部渠道' },
{ value: 'wechat', label: '微信' },
{ value: 'whatsapp', label: 'WhatsApp' },
] as const
function ymd(d: Date): number {
return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate()
}
function addDays(d: Date, n: number): Date {
const x = new Date(d)
x.setDate(x.getDate() + n)
return x
}
/** 预设 → [start,end] yyyymmdd。 */
export function presetRange(p: Preset, today = new Date()): { start: number; end: number } {
const t0 = new Date(today.getFullYear(), today.getMonth(), today.getDate())
switch (p) {
case 'last7':
return { start: ymd(addDays(t0, -6)), end: ymd(t0) }
case 'last30':
return { start: ymd(addDays(t0, -29)), end: ymd(t0) }
case 'thisMonth':
return { start: ymd(new Date(t0.getFullYear(), t0.getMonth(), 1)), end: ymd(t0) }
case 'lastMonth': {
const first = new Date(t0.getFullYear(), t0.getMonth() - 1, 1)
const last = new Date(t0.getFullYear(), t0.getMonth(), 0)
return { start: ymd(first), end: ymd(last) }
}
}
// custom/未知:今天单日
return { start: ymd(t0), end: ymd(t0) }
}
export function useManagementFilters() {
const route = useRoute()
const router = useRouter()
const channel = computed<'all' | 'wechat' | 'whatsapp'>(() => {
const v = route.query.channel
return v === 'wechat' || v === 'whatsapp' ? v : 'all'
})
const start = computed(() => {
const v = Number(route.query.start)
return Number.isFinite(v) && v >= 20000101 && v <= 99991231 ? v : presetRange('last30').start
})
const end = computed(() => {
const v = Number(route.query.end)
return Number.isFinite(v) && v >= 20000101 && v <= 99991231 ? v : presetRange('last30').end
})
const compare = computed(() => {
const v = route.query.compare
return v === 'personal_baseline' || v === 'team_median' ? v : 'previous'
})
function apply(patch: Record<string, string | number | undefined>) {
const q: Record<string, string> = {}
for (const [k, v] of Object.entries({ ...route.query, ...patch })) {
if (v !== undefined && v !== null && v !== '') q[k] = String(v)
}
router.replace({ query: q })
}
// 预设切换:清掉 start/end 让默认值生效,custom 则带具体日期
function applyPreset(p: Preset, custom?: { start: number; end: number }) {
if (p === 'custom' && custom) apply({ start: custom.start, end: custom.end })
else apply({ start: undefined, end: undefined })
}
return { channel, start, end, compare, apply, applyPreset, presetRange }
}