Files
console-dev/app/lib/useListPage.js
T

188 lines
5.1 KiB
JavaScript

/**
* 列表页面共用加载器:分页、同请求去重、身份隔离、缓存首屏与刷新失败保留旧结果。
* 缓存来自统一 auth 模块;旧查询/旧账号/已卸载组件的响应不能覆盖当前列表。
*/
import { onMounted } from "vue";
import { watch } from "vue";
import { onScopeDispose } from "vue";
import { ref } from "vue";
import { apiCacheKey } from "~/lib/auth.js";
import { apiCacheVersion } from "~/lib/auth.js";
import { useApi } from "~/lib/auth.js";
import { writeApiCache } from "~/lib/auth.js";
import { readApiCache } from "~/lib/auth.js";
import { clearApiCache } from "~/lib/auth.js";
/** u 为接口路径,d 可提供查询参数、外部 loader、行键和缓存命名空间等现有列表选项。 */
function u(u, d = {}) {
let f = useApi(),
p =
d.identity ??
(typeof f.identity == `function` ? () => f.identity() : void 0),
m = d.retainOnRefreshError !== !1,
h = ref([]),
g = ref(0),
_ = ref(!1),
v = ref(``),
y = ref(!1),
b = ref(1),
x = ref(d.limit ?? 10),
S = 0,
C = ``,
w = !0,
T = !1,
E = null;
// 身份变更或查询构造失败时统一作废在途序号,清理可见数据,防止跨账号残留。
function D() {
(S++,
(h.value = []),
(g.value = 0),
(_.value = !1),
(v.value = ``),
(y.value = !1),
(C = ``),
(E = null),
(w = !1));
}
(p &&
watch(
p,
() => {
(clearApiCache(), D());
},
{
flush: `sync`,
},
),
onScopeDispose(() => {
((T = !0), S++, (E = null));
}));
// 优先使用业务行键,否则使用 id;没有稳定键的行不做对象复用。
function O(e) {
if (d.rowKey) return d.rowKey(e);
if (
e &&
typeof e == `object` &&
`id` in e &&
(typeof e.id == `string` || typeof e.id == `number`)
)
return e.id;
}
/** 构造查询快照、读取一次初始缓存,再发真实请求刷新;同参数同身份共享 Promise。 */
function k() {
if (T) return Promise.resolve();
let e,
t,
n = p?.();
try {
((e = {
page: b.value,
limit: x.value,
...(d.params?.() ?? {}),
}),
(t = apiCacheKey(
u,
e,
typeof d.snapshotKey == `function` ? d.snapshotKey() : d.snapshotKey,
)));
} catch (e) {
return (
D(),
(v.value = e instanceof Error ? e.message : `查询条件无效`),
Promise.resolve()
);
}
if (E?.query === t && E.identity === n) return E.promise;
let r = ++S,
o = () => !T && r === S && (!p || p() === n);
((_.value = !0), (v.value = ``));
let l = y.value && t === C,
k = p ? readApiCache(n, t) : void 0,
A = apiCacheVersion();
(l || ((h.value = []), (g.value = 0), (y.value = !1)),
w &&
k &&
((h.value = k.list),
(g.value = k.count),
(y.value = !0),
(C = t),
(l = !0)),
(w = !1));
let j = (async () => {
try {
let r = d.loader ? await d.loader(e) : await f.get(u, e);
if (!o()) return;
// 复用同 id 的行对象以保留 UI 引用,但删除新响应已不存在的字段,避免旧值残留。
let i = new Map(
h.value.flatMap((e) => {
let t = O(e);
return t === void 0 ? [] : [[t, e]];
}),
);
((h.value = (r.list ?? []).map((e) => {
let t = O(e),
n = t === void 0 ? void 0 : i.get(t);
if (!n || typeof n != `object` || typeof e != `object` || !e)
return e;
let r = n;
for (let t of Object.keys(r))
Object.prototype.hasOwnProperty.call(e, t) || delete r[t];
return (Object.assign(r, e), n);
})),
(g.value = r.count ?? 0),
(y.value = !0),
(C = t),
p &&
writeApiCache(
n,
t,
{
list: h.value,
count: g.value,
},
A,
));
} catch (e) {
// 只有同一查询已拥有结果时才可保留旧列表;新筛选失败不能继续展示上一查询。
if (!o()) return;
((v.value = e instanceof Error ? e.message : `加载失败`),
(!m || !l) && ((h.value = []), (g.value = 0), (y.value = !1)));
} finally {
(o() && (_.value = !1), r === S && (E = null));
}
})();
return (
o() &&
_.value &&
(E = {
query: t,
identity: n,
promise: j,
}),
j
);
}
// PrimeVue 页码从 0 开始,后端接口从 1 开始;切筛选的 reload 则回到第一页。
function A(e) {
((b.value = e.page + 1), (x.value = e.rows), k());
}
function j() {
((b.value = 1), k());
}
return (
d.immediate !== !1 && onMounted(k),
{
list: h,
total: g,
loading: _,
error: v,
hasResult: y,
page: b,
limit: x,
load: k,
onPage: A,
reload: j,
}
);
}
export { u as t };