Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
117 lines
2.8 KiB
TypeScript
117 lines
2.8 KiB
TypeScript
import { ref, watch, nextTick, type Ref } from 'vue'
|
|
import { useRoute, useRouter } from '#imports'
|
|
|
|
interface ParamDef<T extends string | number = string | number> {
|
|
default: T
|
|
type?: 'string' | 'number'
|
|
/** Debounce URL writes (ms). Default: 0 (immediate). */
|
|
debounce?: number
|
|
}
|
|
|
|
type ParamDefs = Record<string, ParamDef>
|
|
|
|
type InferRef<D extends ParamDef> = D['default'] extends number ? Ref<number> : Ref<string>
|
|
|
|
type StateRefs<T extends ParamDefs> = {
|
|
[K in keyof T]: InferRef<T[K]>
|
|
}
|
|
|
|
interface UseUrlStateOptions {
|
|
/** Called when state is restored from URL (back/forward navigation). */
|
|
onRestore?: () => void
|
|
}
|
|
|
|
export function useUrlState<T extends ParamDefs>(
|
|
params: T,
|
|
options?: UseUrlStateOptions,
|
|
): StateRefs<T> {
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const keys = Object.keys(params) as (keyof T & string)[]
|
|
const refs: Record<string, Ref<string | number>> = {}
|
|
const timers: Record<string, ReturnType<typeof setTimeout> | null> = {}
|
|
|
|
for (const key of keys) {
|
|
refs[key] = ref(parseValue(route.query[key], params[key]!))
|
|
timers[key] = null
|
|
}
|
|
|
|
let isProgrammatic = false
|
|
|
|
const buildQuery = (): Record<string, string> => {
|
|
const q: Record<string, string> = {}
|
|
for (const key of keys) {
|
|
const val = refs[key]!.value
|
|
if (val !== params[key]!.default) {
|
|
q[key] = String(val)
|
|
}
|
|
}
|
|
return q
|
|
}
|
|
|
|
const pushToUrl = () => {
|
|
if (isProgrammatic) return
|
|
isProgrammatic = true
|
|
const query = buildQuery()
|
|
router
|
|
.replace({ path: route.path, query })
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
nextTick(() => {
|
|
isProgrammatic = false
|
|
})
|
|
})
|
|
}
|
|
|
|
for (const key of keys) {
|
|
const ms = params[key]!.debounce ?? 0
|
|
watch(refs[key]!, () => {
|
|
if (isProgrammatic) return
|
|
if (ms > 0) {
|
|
if (timers[key]) clearTimeout(timers[key]!)
|
|
timers[key] = setTimeout(pushToUrl, ms)
|
|
} else {
|
|
pushToUrl()
|
|
}
|
|
})
|
|
}
|
|
|
|
watch(
|
|
() => ({ ...route.query }),
|
|
(newQuery) => {
|
|
if (isProgrammatic) return
|
|
isProgrammatic = true
|
|
let changed = false
|
|
for (const key of keys) {
|
|
const parsed = parseValue(newQuery[key], params[key]!)
|
|
if (refs[key]!.value !== parsed) {
|
|
refs[key]!.value = parsed
|
|
changed = true
|
|
}
|
|
}
|
|
nextTick(() => {
|
|
isProgrammatic = false
|
|
if (changed && options?.onRestore) {
|
|
options.onRestore()
|
|
}
|
|
})
|
|
},
|
|
)
|
|
|
|
return refs as StateRefs<T>
|
|
}
|
|
|
|
function parseValue(
|
|
raw: unknown,
|
|
def: ParamDef,
|
|
): string | number {
|
|
const str = typeof raw === 'string' ? raw : null
|
|
if (str === null) return def.default
|
|
if (def.type === 'number' || typeof def.default === 'number') {
|
|
const n = Number(str)
|
|
return Number.isFinite(n) ? n : def.default
|
|
}
|
|
return str
|
|
}
|