Skip to content

路由同步

路由同步让搜索条件、分页等状态写入 URL query,实现"可分享链接"和"刷新不丢条件"。

方式一:AutoCrud 一行开启

AutoCrud 上加两个 prop 即可:

vue
<AutoCrud
  :adapter="adapter"
  :fields="fields"
  :columns="columns"
  search-query-key="search"
  route-sync
  route-query-key="q"
/>
Prop类型默认值说明
routeSyncbooleanfalse开启路由同步
routeQueryKeystring'q'URL query 参数名

开启后,URL 会变为 ?q={"search":{"name":"xxx"}} 这样的形式。刷新页面后条件自动还原。

WARNING

routeSync 需要组件运行在 vue-router 上下文里(能拿到 useRoute / useRouter)。

方式二:手动使用 useCrudRouteSync

当你使用 hooks 自由组合时,可以直接调用 useCrudRouteSync

vue
<script setup lang="ts">
import type { DemoQuery, DemoRow } from './basic-types'
import { useCrudList, useCrudRouteSync } from '@uozi/vito-core'
import { useRoute, useRouter } from 'vue-router'
import { createBasicAdapter } from './basic-adapter'

const route = useRoute()
const router = useRouter()

const { adapter } = createBasicAdapter()

const list = useCrudList<DemoRow, DemoQuery>({
  adapter,
})

useCrudRouteSync<DemoQuery>({
  query: list.query,
  setQuery: list.setQuery,
  router,
  route,
  queryKey: 'q',
})
</script>

<template>
  <div>
    <!-- 这里只演示 route-sync 的核心用法,渲染由你自行决定 -->
  </div>
</template>
ts
import type { CrudAdapter, CrudSort, ListResult } from '@uozi/vito-core'
import type { DemoQuery, DemoRow } from './basic-types'

function compare(a: unknown, b: unknown): number {
  if (a === b)
    return 0
  if (a === null || a === undefined)
    return -1
  if (b === null || b === undefined)
    return 1
  if (typeof a === 'number' && typeof b === 'number')
    return a - b
  return String(a).localeCompare(String(b))
}

function sortRows(rows: DemoRow[], sort?: CrudSort | null): DemoRow[] {
  if (!sort)
    return rows
  const dir = sort.order === 'descend' ? -1 : 1
  return rows.slice().sort((ra, rb) => dir * compare((ra as any)[sort.field], (rb as any)[sort.field]))
}

function filterRows(rows: DemoRow[], query: DemoQuery): DemoRow[] {
  const s = query.search ?? {}
  const name = s.name?.trim() ?? null
  const status = s.status ?? null
  return rows.filter((r) => {
    if (name && !r.name.toLowerCase().includes(name.toLowerCase()))
      return false
    if (status && r.status !== status)
      return false
    return true
  })
}

function createSeed(): DemoRow[] {
  const now = Date.now()
  return [
    { id: 1, name: '示例 1', status: 'enabled', amount: 12.3, createdAt: now - 3600_000 },
    { id: 2, name: '示例 2', status: 'draft', amount: 45.6, createdAt: now - 7200_000 },
    { id: 3, name: '示例 3', status: 'disabled', amount: 78.9, createdAt: now - 10800_000 },
  ]
}

export function createBasicAdapter(initial?: DemoRow[]): {
  adapter: CrudAdapter<DemoRow, DemoQuery>
  reset: (next?: DemoRow[]) => void
  getAll: () => DemoRow[]
} {
  let db = (initial ?? createSeed()).slice()
  let idSeq = db.reduce((m, r) => Math.max(m, r.id), 0) + 1

  function getAll() {
    return db.slice()
  }

  function reset(next?: DemoRow[]) {
    db = (next ?? createSeed()).slice()
    idSeq = db.reduce((m, r) => Math.max(m, r.id), 0) + 1
  }

  const adapter: CrudAdapter<DemoRow, DemoQuery> = {
    getId(row) {
      return row.id
    },
    async list(params): Promise<ListResult<DemoRow>> {
      const filtered = filterRows(db, params.query)
      const sorted = sortRows(filtered, params.sort)
      const page = Math.max(1, params.page)
      const pageSize = Math.max(1, params.pageSize)
      const start = (page - 1) * pageSize
      return {
        items: sorted.slice(start, start + pageSize),
        total: sorted.length,
      }
    },
    async create(data): Promise<DemoRow> {
      const row: DemoRow = {
        id: idSeq++,
        name: String((data as any)?.name ?? ''),
        status: ((data as any)?.status ?? 'draft') as DemoRow['status'],
        amount: Number((data as any)?.amount ?? 0),
        createdAt: Date.now(),
      }
      db = [row, ...db]
      return row
    },
    async update(id, data): Promise<DemoRow> {
      const idx = db.findIndex(r => r.id === id)
      if (idx < 0)
        throw new Error(`记录不存在:${id}`)
      const next: DemoRow = { ...db[idx], ...(data as any), id: db[idx].id }
      db = db.slice()
      db[idx] = next
      return next
    },
    async remove(id): Promise<void> {
      db = db.filter(r => r.id !== id)
    },
  }

  return { adapter, reset, getAll }
}
ts
export interface DemoRow {
  id: number
  name: string
  status: 'draft' | 'enabled' | 'disabled'
  amount: number
  createdAt: number
}

export interface DemoQuery {
  search?: {
    name?: string | null
    status?: DemoRow['status'] | null
  }
}

配置项

参数类型默认值说明
queryRef<Query>来自 useCrudList 的 query ref
setQuery(partial: Partial<Query>, options?: SetQueryOptions) => void来自 useCrudList 的 setQuery
routerRoutervue-router 的 router 实例
routeRouteLocationNormalizedvue-router 的 route 实例
queryKeystring'q'URL query 参数名
serialize(query: Query) => stringJSON.stringify自定义序列化
deserialize(str: string) => Partial<Query>JSON.parse自定义反序列化
debounceMsnumber300写入 URL 的防抖延迟(ms)
syncFromRouteMode'replace' | 'merge''replace'从 URL 还原时的写回策略(默认以 URL 为准)

行为说明

  1. 初始化:组件挂载时从 route.query[queryKey] 读取并反序列化,写回 setQuery
  2. 写入 URL:监听 query 变化,防抖后调用 router.replace({ query })
  3. 防循环:从路由读取时会设置标记,避免写回时触发 watch 循环
  4. 空值清理:深度裁剪空值后若 query 为空,自动删除 URL 中的 queryKey 参数(保留 0/false

自定义序列化

默认使用 JSON.stringify / JSON.parse。如果你想用更紧凑的格式(如 qs 库),可以传入自定义函数:

ts
import qs from 'qs'

useCrudRouteSync({
  query: list.query,
  setQuery: list.setQuery,
  router,
  route,
  serialize: q => qs.stringify(q),
  deserialize: s => qs.parse(s),
})

Made with VitePress