提交
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { addWatchlist, getStockFacets, getStocks, removeWatchlist } from '@/api/client';
|
||||
import type { FacetItem, ScreenerItemOut, StockListItem } from '@/api/types';
|
||||
import { useQuerySync } from '@/composables/useQuerySync';
|
||||
import StockDetailOverlay from '@/components/StockDetailOverlay.vue';
|
||||
|
||||
// ---------- 筛选状态(初始值从路由 query 还原,刷新/分享链接不丢现场) ----------
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function qStr(key: string): string | undefined {
|
||||
const v = route.query[key];
|
||||
@@ -60,10 +60,14 @@ const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize))
|
||||
|
||||
// ---------- 加载(搜索防抖) ----------
|
||||
let fetchToken = 0;
|
||||
let fetchCtrl: AbortController | null = null;
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function load() {
|
||||
const token = ++fetchToken;
|
||||
fetchCtrl?.abort();
|
||||
const ctrl = new AbortController();
|
||||
fetchCtrl = ctrl;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
@@ -78,13 +82,17 @@ async function load() {
|
||||
order: order.value,
|
||||
limit: pageSize,
|
||||
offset: (page.value - 1) * pageSize,
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (token === fetchToken) {
|
||||
items.value = res.items;
|
||||
total.value = res.total;
|
||||
}
|
||||
} catch (e) {
|
||||
if (token === fetchToken) error.value = e instanceof Error ? e.message : '加载失败';
|
||||
// AbortError = 被更新的请求取消,静默(token 检查通常已挡住,这里双保险)
|
||||
if (token === fetchToken && !(e instanceof Error && e.name === 'AbortError')) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败';
|
||||
}
|
||||
} finally {
|
||||
if (token === fetchToken) loading.value = false;
|
||||
}
|
||||
@@ -191,47 +199,34 @@ function onWatchedChange() {
|
||||
load();
|
||||
}
|
||||
|
||||
// ---------- 路由同步:状态 → ?q/&market/…(replace 不产生历史记录) ----------
|
||||
function buildQuery(): Record<string, string> {
|
||||
const q: Record<string, string> = {};
|
||||
if (search.value.trim()) q.q = search.value.trim();
|
||||
if (market.value !== '全部') q.market = market.value;
|
||||
if (industry.value) q.industry = industry.value;
|
||||
if (area.value) q.area = area.value;
|
||||
if (sort.value !== 'symbol') q.sort = sort.value;
|
||||
if (order.value !== 'asc') q.order = order.value;
|
||||
if (page.value > 1) q.page = String(page.value);
|
||||
if (previewCode.value) q.code = previewCode.value;
|
||||
return q;
|
||||
}
|
||||
|
||||
let selfNav = 0; // 自己发起的导航在途数量:其 route 变化不回灌状态(防输入被旧 URL 覆盖)
|
||||
function syncRoute(push = false) {
|
||||
const query = buildQuery();
|
||||
// 与当前 URL 一致就跳过,避免 state→route→state 回声
|
||||
if (JSON.stringify(query) === JSON.stringify(route.query)) return;
|
||||
selfNav++;
|
||||
const done = () => { selfNav--; };
|
||||
void (push ? router.push({ query }) : router.replace({ query })).then(done, done);
|
||||
}
|
||||
|
||||
// 列表状态变化(含搜索防抖外的输入)随手回写 URL;翻页/筛选也带着当前 ?code
|
||||
watch([search, market, industry, area, sort, order, page], () => syncRoute());
|
||||
|
||||
// 浏览器前进/后退(含返回键关掉 ?code=):把 query 应用回状态
|
||||
watch(() => route.query, (q) => {
|
||||
if (selfNav > 0) return;
|
||||
const qOf = (k: string) => (typeof q[k] === 'string' ? (q[k] as string) : '');
|
||||
search.value = qOf('q');
|
||||
market.value = MARKETS.includes(qOf('market')) ? qOf('market') : '全部';
|
||||
industry.value = qOf('industry');
|
||||
area.value = qOf('area');
|
||||
const p = parseInt(qOf('page'), 10);
|
||||
page.value = Number.isFinite(p) && p >= 1 ? p : 1;
|
||||
const s = qOf('sort');
|
||||
sort.value = SORT_KEYS.includes(s as SortKey) ? (s as SortKey) : 'symbol';
|
||||
order.value = qOf('order') === 'desc' ? 'desc' : 'asc';
|
||||
previewCode.value = qOf('code') || null;
|
||||
// ---------- 路由同步:状态 → ?q/&market/…(骨架在 useQuerySync,replace 不产生历史记录) ----------
|
||||
const { syncRoute } = useQuerySync({
|
||||
sources: [search, market, industry, area, sort, order, page],
|
||||
buildQuery: () => {
|
||||
const q: Record<string, string> = {};
|
||||
if (search.value.trim()) q.q = search.value.trim();
|
||||
if (market.value !== '全部') q.market = market.value;
|
||||
if (industry.value) q.industry = industry.value;
|
||||
if (area.value) q.area = area.value;
|
||||
if (sort.value !== 'symbol') q.sort = sort.value;
|
||||
if (order.value !== 'asc') q.order = order.value;
|
||||
if (page.value > 1) q.page = String(page.value);
|
||||
if (previewCode.value) q.code = previewCode.value;
|
||||
return q;
|
||||
},
|
||||
// 浏览器前进/后退(含返回键关掉 ?code=):把 query 应用回状态
|
||||
applyQuery: (q) => {
|
||||
search.value = q.q ?? '';
|
||||
market.value = MARKETS.includes(q.market ?? '') ? (q.market as typeof MARKETS[number]) : '全部';
|
||||
industry.value = q.industry ?? '';
|
||||
area.value = q.area ?? '';
|
||||
const p = parseInt(q.page ?? '', 10);
|
||||
page.value = Number.isFinite(p) && p >= 1 ? p : 1;
|
||||
const s = q.sort ?? 'symbol';
|
||||
sort.value = SORT_KEYS.includes(s as SortKey) ? (s as SortKey) : 'symbol';
|
||||
order.value = q.order === 'desc' ? 'desc' : 'asc';
|
||||
previewCode.value = q.code || null;
|
||||
},
|
||||
});
|
||||
|
||||
// ---------- 详情浮层开关(写入 ?code=) ----------
|
||||
|
||||
Reference in New Issue
Block a user