优化构架

This commit is contained in:
2026-08-03 18:49:18 +08:00
parent 1241d11c01
commit 44c33a6280
133 changed files with 651 additions and 603 deletions
+83
View File
@@ -0,0 +1,83 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
describe('unsaved work store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('blocks dirty work only when a role switch removes permission or leaves the page', () => {
const store = useUnsavedWorkStore()
store.upsert({
key: 'customer-form',
label: '客户资料',
dirty: true,
requiredPermission: 'customer:create',
})
store.upsert({
key: 'order-form',
label: '头程订单',
dirty: true,
requiredPermission: 'first-leg:create',
})
expect(store.findBlocking(new Set(['customer:create', 'first-leg:create']), false)).toEqual([])
expect(store.findBlocking(new Set(), false).map((entry) => entry.key)).toEqual([
'customer-form',
'order-form',
])
expect(store.findBlocking(new Set(['customer:create', 'first-leg:create']), true)
.map((entry) => entry.key)).toEqual(['customer-form', 'order-form'])
})
it('does not block clean work', () => {
const store = useUnsavedWorkStore()
store.upsert({
key: 'customer-form',
label: '客户资料',
dirty: false,
requiredPermission: 'customer:create',
})
expect(store.findBlocking(new Set(), true)).toEqual([])
})
it('removes an entry by key', () => {
const store = useUnsavedWorkStore()
store.upsert({ key: 'customer-form', label: '客户资料', dirty: true, requiredPermission: null })
store.remove('customer-form')
expect(store.entries).toEqual([])
})
it('replaces an existing entry with the same key', () => {
const store = useUnsavedWorkStore()
store.upsert({ key: 'customer-form', label: '旧名称', dirty: false, requiredPermission: null })
store.upsert({
key: 'customer-form',
label: '客户资料',
dirty: true,
requiredPermission: 'customer:create',
})
expect(store.entries).toEqual([{
key: 'customer-form',
label: '客户资料',
dirty: true,
requiredPermission: 'customer:create',
}])
})
it('clears every registered entry', () => {
const store = useUnsavedWorkStore()
store.upsert({ key: 'customer-form', label: '客户资料', dirty: true, requiredPermission: null })
store.upsert({ key: 'order-form', label: '头程订单', dirty: true, requiredPermission: null })
store.clear()
expect(store.entries).toEqual([])
})
})
+40
View File
@@ -0,0 +1,40 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export interface UnsavedWorkEntry {
key: string
label: string
dirty: boolean
requiredPermission: string | null
}
export const useUnsavedWorkStore = defineStore('unsaved-work', () => {
const entries = ref<UnsavedWorkEntry[]>([])
function upsert(entry: UnsavedWorkEntry) {
const index = entries.value.findIndex((item) => item.key === entry.key)
if (index === -1) {
entries.value.push(entry)
return
}
entries.value[index] = entry
}
function remove(key: string) {
const index = entries.value.findIndex((item) => item.key === key)
if (index !== -1) entries.value.splice(index, 1)
}
function findBlocking(candidatePermissions: ReadonlySet<string>, willLeave: boolean) {
return entries.value.filter((entry) => entry.dirty && (
willLeave
|| (entry.requiredPermission !== null && !candidatePermissions.has(entry.requiredPermission))
))
}
function clear() {
entries.value = []
}
return { entries, upsert, remove, findBlocking, clear }
})
@@ -0,0 +1,101 @@
import { computed, defineComponent, nextTick, reactive, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
interface HarnessModel {
name: string
}
function mountHarness() {
const isOpen = ref(false)
const label = ref('客户资料')
const requiredPermission = ref<string | null>('customer:create')
const model = reactive<HarnessModel>({ name: '' })
let captureBaseline: () => void = () => undefined
const wrapper = mount(defineComponent({
setup() {
const registration = useUnsavedWorkRegistration({
key: 'customer-form',
label: computed(() => label.value),
isOpen,
model,
requiredPermission: computed(() => requiredPermission.value),
})
captureBaseline = registration.captureBaseline
return () => null
},
}))
return { wrapper, isOpen, label, requiredPermission, model, captureBaseline }
}
describe('useUnsavedWorkRegistration', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('tracks open model changes against a captured baseline and removes closed work', async () => {
const store = useUnsavedWorkStore()
const harness = mountHarness()
harness.captureBaseline()
expect(store.entries).toEqual([])
harness.isOpen.value = true
await nextTick()
expect(store.entries[0]?.dirty).toBe(false)
harness.model.name = '柏悦物流'
await nextTick()
expect(store.entries[0]?.dirty).toBe(true)
harness.isOpen.value = false
await nextTick()
expect(store.entries).toEqual([])
harness.wrapper.unmount()
})
it('updates label and permission reactively and becomes clean after restoring the baseline', async () => {
const store = useUnsavedWorkStore()
const harness = mountHarness()
harness.captureBaseline()
harness.isOpen.value = true
await nextTick()
harness.label.value = '新增客户'
harness.requiredPermission.value = 'customer:update'
harness.model.name = '柏悦物流'
await nextTick()
expect(store.entries[0]).toEqual({
key: 'customer-form',
label: '新增客户',
dirty: true,
requiredPermission: 'customer:update',
})
harness.model.name = ''
await nextTick()
expect(store.entries[0]?.dirty).toBe(false)
harness.wrapper.unmount()
})
it('removes registered work when its component scope is disposed', async () => {
const store = useUnsavedWorkStore()
const harness = mountHarness()
harness.captureBaseline()
harness.isOpen.value = true
harness.model.name = '柏悦物流'
await nextTick()
expect(store.entries).toHaveLength(1)
harness.wrapper.unmount()
expect(store.entries).toEqual([])
})
})
@@ -0,0 +1,47 @@
import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
interface UnsavedWorkRegistrationOptions<T extends object> {
key: string
label: ComputedRef<string>
isOpen: Ref<boolean>
model: T
requiredPermission: ComputedRef<string | null>
}
export function useUnsavedWorkRegistration<T extends object>(
options: UnsavedWorkRegistrationOptions<T>,
) {
const store = useUnsavedWorkStore()
const snapshot = computed(() => JSON.stringify(options.model))
const baseline = ref(snapshot.value)
function sync() {
if (!options.isOpen.value) {
store.remove(options.key)
return
}
store.upsert({
key: options.key,
label: options.label.value,
dirty: snapshot.value !== baseline.value,
requiredPermission: options.requiredPermission.value,
})
}
function captureBaseline() {
baseline.value = snapshot.value
sync()
}
watch(
[options.isOpen, snapshot, options.label, options.requiredPermission],
sync,
{ flush: 'sync' },
)
sync()
onScopeDispose(() => store.remove(options.key))
return { captureBaseline }
}