feat(management): useManagementFilters——URL query 真源/预设窗/渠道哨兵
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user