This commit is contained in:
2026-08-05 15:54:59 +08:00
parent eede351038
commit 4b31eb7786
43 changed files with 2117 additions and 851 deletions
+33
View File
@@ -71,6 +71,27 @@ describe('unsaved work store', () => {
}])
})
it('does not let an obsolete component remove or overwrite a reclaimed registration', () => {
const store = useUnsavedWorkStore()
const oldOwner = Symbol('old')
const newOwner = Symbol('new')
store.claim('customer-form', oldOwner)
store.upsert({ key: 'customer-form', label: '旧页面', dirty: true, requiredPermission: null }, oldOwner)
store.claim('customer-form', newOwner)
store.upsert({ key: 'customer-form', label: '新页面', dirty: true, requiredPermission: null }, newOwner)
store.remove('customer-form', oldOwner)
store.release('customer-form', oldOwner)
store.upsert({ key: 'customer-form', label: '过期页面', dirty: false, requiredPermission: null }, oldOwner)
expect(store.entries).toEqual([{
key: 'customer-form',
label: '新页面',
dirty: true,
requiredPermission: null,
}])
})
it('clears every registered entry', () => {
const store = useUnsavedWorkStore()
store.upsert({ key: 'customer-form', label: '客户资料', dirty: true, requiredPermission: null })
@@ -80,4 +101,16 @@ describe('unsaved work store', () => {
expect(store.entries).toEqual([])
})
it('finds and removes dirty work owned by a workspace tab', () => {
const store = useUnsavedWorkStore()
store.upsert({ key: 'customer-form', label: '客户资料', dirty: true, requiredPermission: null, workspaceTabId: 'customers' })
store.upsert({ key: 'warehouse-form', label: '仓库资料', dirty: true, requiredPermission: null, workspaceTabId: 'warehouses' })
expect(store.findForWorkspace('customers').map((entry) => entry.key)).toEqual(['customer-form'])
expect(store.dirtyWorkspaceIds).toEqual(new Set(['customers', 'warehouses']))
store.removeWorkspaces(['customers'])
expect(store.entries.map((entry) => entry.key)).toEqual(['warehouse-form'])
})
})
+39 -4
View File
@@ -1,4 +1,4 @@
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
export interface UnsavedWorkEntry {
@@ -6,12 +6,19 @@ export interface UnsavedWorkEntry {
label: string
dirty: boolean
requiredPermission: string | null
workspaceTabId?: string | null
}
export const useUnsavedWorkStore = defineStore('unsaved-work', () => {
const entries = ref<UnsavedWorkEntry[]>([])
const owners = new Map<string, symbol>()
function upsert(entry: UnsavedWorkEntry) {
function claim(key: string, owner: symbol) {
owners.set(key, owner)
}
function upsert(entry: UnsavedWorkEntry, owner?: symbol) {
if (owner && owners.get(entry.key) !== owner) return
const index = entries.value.findIndex((item) => item.key === entry.key)
if (index === -1) {
entries.value.push(entry)
@@ -20,7 +27,16 @@ export const useUnsavedWorkStore = defineStore('unsaved-work', () => {
entries.value[index] = entry
}
function remove(key: string) {
function remove(key: string, owner?: symbol) {
if (owner && owners.get(key) !== owner) return
if (!owner) owners.delete(key)
const index = entries.value.findIndex((item) => item.key === key)
if (index !== -1) entries.value.splice(index, 1)
}
function release(key: string, owner: symbol) {
if (owners.get(key) !== owner) return
owners.delete(key)
const index = entries.value.findIndex((item) => item.key === key)
if (index !== -1) entries.value.splice(index, 1)
}
@@ -32,9 +48,28 @@ export const useUnsavedWorkStore = defineStore('unsaved-work', () => {
))
}
function findForWorkspace(workspaceTabId: string) {
return entries.value.filter((entry) => entry.dirty && entry.workspaceTabId === workspaceTabId)
}
function removeWorkspaces(workspaceTabIds: readonly string[]) {
const ids = new Set(workspaceTabIds)
entries.value
.filter((entry) => entry.workspaceTabId && ids.has(entry.workspaceTabId))
.forEach((entry) => owners.delete(entry.key))
entries.value = entries.value.filter((entry) => !entry.workspaceTabId || !ids.has(entry.workspaceTabId))
}
function clear() {
owners.clear()
entries.value = []
}
return { entries, upsert, remove, findBlocking, clear }
const dirtyWorkspaceIds = computed<ReadonlySet<string>>(() => new Set(
entries.value
.filter((entry) => entry.dirty && entry.workspaceTabId)
.map((entry) => entry.workspaceTabId as string),
))
return { entries, dirtyWorkspaceIds, claim, upsert, remove, release, findBlocking, findForWorkspace, removeWorkspaces, clear }
})
@@ -76,6 +76,7 @@ describe('useUnsavedWorkRegistration', () => {
label: '新增客户',
dirty: true,
requiredPermission: 'customer:update',
workspaceTabId: null,
})
harness.model.name = ''
@@ -1,4 +1,4 @@
import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
import { computed, getCurrentInstance, onActivated, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
interface UnsavedWorkRegistrationOptions<T extends object> {
@@ -13,12 +13,16 @@ export function useUnsavedWorkRegistration<T extends object>(
options: UnsavedWorkRegistrationOptions<T>,
) {
const store = useUnsavedWorkStore()
const routeName = getCurrentInstance()?.proxy?.$route?.name
const workspaceTabId = routeName === undefined || routeName === null ? null : String(routeName)
const owner = Symbol(options.key)
const snapshot = computed(() => JSON.stringify(options.model))
const baseline = ref(snapshot.value)
store.claim(options.key, owner)
function sync() {
if (!options.isOpen.value) {
store.remove(options.key)
store.remove(options.key, owner)
return
}
@@ -27,7 +31,8 @@ export function useUnsavedWorkRegistration<T extends object>(
label: options.label.value,
dirty: snapshot.value !== baseline.value,
requiredPermission: options.requiredPermission.value,
})
workspaceTabId,
}, owner)
}
function captureBaseline() {
@@ -41,7 +46,8 @@ export function useUnsavedWorkRegistration<T extends object>(
{ flush: 'sync' },
)
sync()
onScopeDispose(() => store.remove(options.key))
onActivated(sync)
onScopeDispose(() => store.release(options.key, owner))
return { captureBaseline }
}