看股功能更新
This commit is contained in:
412
frontend/src/views/StocksView.vue
Normal file
412
frontend/src/views/StocksView.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { addWatchlist, getStockFacets, getStocks, removeWatchlist } from '@/api/client';
|
||||
import type { FacetItem, ScreenerItemOut, StockListItem } from '@/api/types';
|
||||
import StockDetailOverlay from '@/components/StockDetailOverlay.vue';
|
||||
|
||||
// ---------- 筛选状态(初始值从路由 query 还原,刷新/分享链接不丢现场) ----------
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function qStr(key: string): string | undefined {
|
||||
const v = route.query[key];
|
||||
return typeof v === 'string' && v ? v : undefined;
|
||||
}
|
||||
|
||||
const MARKETS = ['全部', '自选', '主板', '创业板', '科创板', '北交所'];
|
||||
// 排序键(后端白名单:symbol/total_mv/circ_mv/pe_ttm/pb/turnover_rate)
|
||||
const SORT_KEYS = ['symbol', 'total_mv', 'circ_mv', 'pe_ttm', 'pb', 'turnover_rate'] as const;
|
||||
type SortKey = (typeof SORT_KEYS)[number];
|
||||
|
||||
const search = ref(qStr('q') ?? '');
|
||||
const market = ref(MARKETS.includes(qStr('market') ?? '') ? (qStr('market') as string) : '全部');
|
||||
const industry = ref(qStr('industry') ?? '');
|
||||
const area = ref(qStr('area') ?? '');
|
||||
const industries = ref<FacetItem[]>([]);
|
||||
const areas = ref<FacetItem[]>([]);
|
||||
const pageSize = 100;
|
||||
const page = ref(Math.max(1, parseInt(qStr('page') ?? '1', 10) || 1));
|
||||
const sortParam = qStr('sort');
|
||||
const sort = ref<SortKey>(SORT_KEYS.includes((sortParam ?? 'symbol') as SortKey) ? ((sortParam ?? 'symbol') as SortKey) : 'symbol');
|
||||
const order = ref<'asc' | 'desc'>(qStr('order') === 'desc' ? 'desc' : 'asc');
|
||||
|
||||
// 列表状态
|
||||
const items = ref<StockListItem[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// 详情浮层(当前股记录在 ?code=,刷新后浮层自动重开)
|
||||
const previewCode = ref<string | null>(qStr('code') ?? null);
|
||||
|
||||
// StockDetailOverlay 需要 ScreenerItemOut 形状;行情字段缺失时它内部有兜底
|
||||
const overlayItems = computed<ScreenerItemOut[]>(() =>
|
||||
items.value.map((it) => ({
|
||||
ts_code: it.ts_code,
|
||||
name: it.name,
|
||||
close: it.close ?? null,
|
||||
pct_chg: it.pct_chg ?? null,
|
||||
total_mv: null,
|
||||
circ_mv: null,
|
||||
pe_ttm: null,
|
||||
pb: null,
|
||||
turnover_rate: null,
|
||||
indicators: {},
|
||||
})),
|
||||
);
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)));
|
||||
|
||||
// ---------- 加载(搜索防抖) ----------
|
||||
let fetchToken = 0;
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function load() {
|
||||
const token = ++fetchToken;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await getStocks({
|
||||
search: search.value.trim(),
|
||||
// 「自选」不是 stock_basic.market 的值,走 watched_only
|
||||
market: market.value === '全部' || market.value === '自选' ? '' : market.value,
|
||||
watched_only: market.value === '自选',
|
||||
industry: industry.value,
|
||||
area: area.value,
|
||||
sort: sort.value,
|
||||
order: order.value,
|
||||
limit: pageSize,
|
||||
offset: (page.value - 1) * pageSize,
|
||||
});
|
||||
if (token === fetchToken) {
|
||||
items.value = res.items;
|
||||
total.value = res.total;
|
||||
}
|
||||
} catch (e) {
|
||||
if (token === fetchToken) error.value = e instanceof Error ? e.message : '加载失败';
|
||||
} finally {
|
||||
if (token === fetchToken) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(search, () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
page.value = 1;
|
||||
load();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 筛选/排序变化回到第一页(page 的 watch 会再触发 load);翻页直接加载
|
||||
watch([market, industry, area, sort, order], () => {
|
||||
if (page.value !== 1) page.value = 1;
|
||||
else load();
|
||||
});
|
||||
watch(page, () => load());
|
||||
load();
|
||||
getStockFacets()
|
||||
.then((f) => {
|
||||
industries.value = f.industries;
|
||||
areas.value = f.areas;
|
||||
})
|
||||
.catch(() => { /* 筛选项加载失败不阻塞列表 */ });
|
||||
onBeforeUnmount(() => clearTimeout(debounceTimer));
|
||||
|
||||
function pctClass(v: number | null | undefined): string {
|
||||
if (v == null) return 'text-[#9BA3AE]';
|
||||
return v > 0 ? 'text-up' : v < 0 ? 'text-down' : 'text-[#A8AFB8]';
|
||||
}
|
||||
|
||||
function fmtPct(v: number | null | undefined): string {
|
||||
if (v == null) return '--';
|
||||
return `${v > 0 ? '+' : ''}${v.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function fmtDate(v: string | null | undefined): string {
|
||||
if (!v) return '--';
|
||||
return v.slice(0, 10);
|
||||
}
|
||||
|
||||
function fmtNum(v: number | null | undefined, digits = 2): string {
|
||||
if (v == null) return '--';
|
||||
return v.toFixed(digits);
|
||||
}
|
||||
|
||||
function fmtYi(v: number | null | undefined): string {
|
||||
if (v == null) return '--';
|
||||
return v >= 100 ? Math.round(v).toLocaleString() : v.toFixed(2);
|
||||
}
|
||||
|
||||
function fmtTurnover(v: number | null | undefined): string {
|
||||
if (v == null) return '--';
|
||||
return `${v.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
// ---------- 列排序(后端白名单键) ----------
|
||||
function toggleSort(key: SortKey) {
|
||||
if (sort.value === key) {
|
||||
order.value = order.value === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sort.value = key;
|
||||
// 代码列默认升序;其余(市值/估值/换手)默认降序——先看最大/最热
|
||||
order.value = key === 'symbol' ? 'asc' : 'desc';
|
||||
}
|
||||
}
|
||||
|
||||
// PE-TTM 分档着色:≤15 冷绿、15-30 中性、30-60 琥珀、>60 红;亏损/无数据灰
|
||||
function peClass(v: number | null | undefined): string {
|
||||
if (v == null || v <= 0) return 'text-[#9BA3AE]';
|
||||
if (v <= 15) return 'text-emerald-400';
|
||||
if (v <= 30) return 'text-[#A8AFB8]';
|
||||
if (v <= 60) return 'text-amber-400';
|
||||
return 'text-red-400';
|
||||
}
|
||||
|
||||
function go(delta: number) {
|
||||
const next = page.value + delta;
|
||||
if (next >= 1 && next <= totalPages.value) page.value = next;
|
||||
}
|
||||
|
||||
// ---------- 自选股星标(服务端为唯一事实源,本地行内即时翻转) ----------
|
||||
const starBusy = ref('');
|
||||
async function toggleStar(it: StockListItem) {
|
||||
if (starBusy.value === it.ts_code) return;
|
||||
starBusy.value = it.ts_code;
|
||||
const wasWatched = it.watched;
|
||||
it.watched = !wasWatched; // 乐观更新
|
||||
try {
|
||||
const list = wasWatched ? await removeWatchlist(it.ts_code) : await addWatchlist(it.ts_code);
|
||||
const set = new Set(list);
|
||||
for (const row of items.value) row.watched = set.has(row.ts_code);
|
||||
} catch {
|
||||
it.watched = wasWatched; // 回滚
|
||||
} finally {
|
||||
starBusy.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 详情浮层里增删自选后,刷新当前页星标
|
||||
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;
|
||||
});
|
||||
|
||||
// ---------- 详情浮层开关(写入 ?code=) ----------
|
||||
function openStock(code: string) {
|
||||
previewCode.value = code;
|
||||
syncRoute(true); // push:浏览器返回键 = 关闭浮层
|
||||
}
|
||||
function onOverlayChange(code: string) {
|
||||
previewCode.value = code; // 浮层内切股(键盘/侧栏)同步到路由
|
||||
syncRoute();
|
||||
}
|
||||
function closeOverlay() {
|
||||
previewCode.value = null;
|
||||
syncRoute();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<h1 class="text-xl font-semibold text-[#E8EAED]">全部股票</h1>
|
||||
<span class="text-[13px] text-[#9BA3AE]">共 {{ total.toLocaleString() }} 只 · 点击行查看 K 线详情</span>
|
||||
|
||||
<div class="relative ml-auto">
|
||||
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#9BA3AE]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" /></svg>
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
placeholder="搜索代码 / 名称"
|
||||
class="w-56 rounded-md border border-[#33353D] bg-[#16181D] py-2 pl-9 pr-3 text-sm text-[#E8EAED] outline-none transition placeholder:text-[#7A818C] focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select v-model="market" class="ipt !w-auto !py-1.5 text-[13px]" title="按板块筛选">
|
||||
<option v-for="m in MARKETS" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
|
||||
<select v-model="industry" class="ipt !w-auto !py-1.5 text-[13px]" title="按行业筛选">
|
||||
<option value="">全部行业</option>
|
||||
<option v-for="i in industries" :key="i.name" :value="i.name">{{ i.name }}({{ i.count }})</option>
|
||||
</select>
|
||||
|
||||
<select v-model="area" class="ipt !w-auto !py-1.5 text-[13px]" title="按地域筛选">
|
||||
<option value="">全部地域</option>
|
||||
<option v-for="a in areas" :key="a.name" :value="a.name">{{ a.name }}({{ a.count }})</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="flex items-start gap-2 rounded-xl border border-red-500/30 bg-red-500/15 px-4 py-3 text-sm text-red-400">
|
||||
<svg class="mt-0.5 h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 3.9L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z" /><path d="M12 9v4M12 17h.01" /></svg>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-[#26272E] bg-[#101014]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-[#1E2026] text-left text-[13px] text-[#A8AFB8]">
|
||||
<th class="w-10 px-2 py-3 font-medium" title="自选">★</th>
|
||||
<th class="px-4 py-3 font-medium">
|
||||
<button type="button" class="inline-flex items-center gap-1 transition hover:text-[#E8EAED]" :class="sort === 'symbol' ? 'text-[#E8EAED]' : ''" @click="toggleSort('symbol')">
|
||||
代码<span class="text-[10px] leading-none" :class="sort === 'symbol' ? 'text-blue-400' : 'text-[#4A4D55]'">{{ sort === 'symbol' ? (order === 'asc' ? '▲' : '▼') : '⇅' }}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-3 font-medium">名称</th>
|
||||
<th class="px-4 py-3 font-medium">行业</th>
|
||||
<th class="px-4 py-3 font-medium">市场</th>
|
||||
<th class="px-4 py-3 text-right font-medium">最新价</th>
|
||||
<th class="px-4 py-3 text-right font-medium">涨跌幅</th>
|
||||
<th class="px-4 py-3 text-right font-medium" title="单位:亿元">
|
||||
<button type="button" class="inline-flex items-center gap-1 transition hover:text-[#E8EAED]" :class="sort === 'total_mv' ? 'text-[#E8EAED]' : ''" @click="toggleSort('total_mv')">
|
||||
总市值<span class="text-[10px] leading-none" :class="sort === 'total_mv' ? 'text-blue-400' : 'text-[#4A4D55]'">{{ sort === 'total_mv' ? (order === 'asc' ? '▲' : '▼') : '⇅' }}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right font-medium" title="单位:亿元">
|
||||
<button type="button" class="inline-flex items-center gap-1 transition hover:text-[#E8EAED]" :class="sort === 'circ_mv' ? 'text-[#E8EAED]' : ''" @click="toggleSort('circ_mv')">
|
||||
流通市值<span class="text-[10px] leading-none" :class="sort === 'circ_mv' ? 'text-blue-400' : 'text-[#4A4D55]'">{{ sort === 'circ_mv' ? (order === 'asc' ? '▲' : '▼') : '⇅' }}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right font-medium" title="≤15 绿 · 15-30 灰 · 30-60 黄 · >60 红;亏损/无数据为空">
|
||||
<button type="button" class="inline-flex items-center gap-1 transition hover:text-[#E8EAED]" :class="sort === 'pe_ttm' ? 'text-[#E8EAED]' : ''" @click="toggleSort('pe_ttm')">
|
||||
市盈率TTM<span class="text-[10px] leading-none" :class="sort === 'pe_ttm' ? 'text-blue-400' : 'text-[#4A4D55]'">{{ sort === 'pe_ttm' ? (order === 'asc' ? '▲' : '▼') : '⇅' }}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right font-medium">
|
||||
<button type="button" class="inline-flex items-center gap-1 transition hover:text-[#E8EAED]" :class="sort === 'pb' ? 'text-[#E8EAED]' : ''" @click="toggleSort('pb')">
|
||||
市净率<span class="text-[10px] leading-none" :class="sort === 'pb' ? 'text-blue-400' : 'text-[#4A4D55]'">{{ sort === 'pb' ? (order === 'asc' ? '▲' : '▼') : '⇅' }}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right font-medium">
|
||||
<button type="button" class="inline-flex items-center gap-1 transition hover:text-[#E8EAED]" :class="sort === 'turnover_rate' ? 'text-[#E8EAED]' : ''" @click="toggleSort('turnover_rate')">
|
||||
换手率<span class="text-[10px] leading-none" :class="sort === 'turnover_rate' ? 'text-blue-400' : 'text-[#4A4D55]'">{{ sort === 'turnover_rate' ? (order === 'asc' ? '▲' : '▼') : '⇅' }}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right font-medium">数据截至</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="loading && items.length === 0">
|
||||
<td colspan="13" class="px-4 py-16 text-center text-[#9BA3AE]">加载中…</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="it in items"
|
||||
:key="it.ts_code"
|
||||
class="cursor-pointer border-b border-[#1E2026] transition hover:bg-blue-500/15"
|
||||
@click="openStock(it.ts_code)"
|
||||
>
|
||||
<td class="px-2 py-2.5 text-center" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-0.5 transition-colors disabled:opacity-50"
|
||||
:class="it.watched ? 'text-amber-500 hover:text-amber-600' : 'text-[#C3C9D2] hover:text-amber-400'"
|
||||
:title="it.watched ? '移出自选' : '加入自选'"
|
||||
:disabled="starBusy === it.ts_code"
|
||||
@click="toggleStar(it)"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" :fill="it.watched ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" stroke-linejoin="round">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 font-mono text-sm text-[#E8EAED]">{{ it.symbol }}</td>
|
||||
<td class="px-4 py-2.5 font-medium text-[#E8EAED]">{{ it.name }}</td>
|
||||
<td class="px-4 py-2.5 text-[#A8AFB8]">{{ it.industry || '--' }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<span class="rounded bg-[#26272E] px-1.5 py-0.5 text-[13px] text-[#A8AFB8]">{{ it.market || '--' }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm font-medium tabular-nums" :class="pctClass(it.pct_chg)">{{ it.close?.toFixed(2) ?? '--' }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm tabular-nums" :class="pctClass(it.pct_chg)">{{ fmtPct(it.pct_chg) }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm tabular-nums text-[#A8AFB8]">{{ fmtYi(it.total_mv) }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm tabular-nums text-[#A8AFB8]">{{ fmtYi(it.circ_mv) }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm tabular-nums" :class="peClass(it.pe_ttm)">{{ fmtNum(it.pe_ttm) }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm tabular-nums text-[#A8AFB8]">{{ fmtNum(it.pb) }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm tabular-nums text-[#A8AFB8]">{{ fmtTurnover(it.turnover_rate) }}</td>
|
||||
<td class="px-4 py-2.5 text-right font-mono text-sm text-[#9BA3AE]">{{ fmtDate(it.last_ts) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && items.length === 0">
|
||||
<td colspan="13" class="px-4 py-16 text-center text-[#9BA3AE]">没有匹配的股票</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-[#1E2026] px-4 py-3 text-[13px] text-[#A8AFB8]">
|
||||
<span v-if="loading">加载中…</span>
|
||||
<span v-else>第 {{ page }} / {{ totalPages }} 页</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-[#26272E] px-3 py-1.5 transition hover:border-[#3A3D46] hover:text-white disabled:opacity-40"
|
||||
:disabled="page <= 1 || loading"
|
||||
@click="go(-1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-[#26272E] px-3 py-1.5 transition hover:border-[#3A3D46] hover:text-white disabled:opacity-40"
|
||||
:disabled="page >= totalPages || loading"
|
||||
@click="go(1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全屏个股详情(与选股页同款;当前股记录在 ?code=) -->
|
||||
<StockDetailOverlay
|
||||
v-if="previewCode && overlayItems.length"
|
||||
:items="overlayItems"
|
||||
:initial="previewCode"
|
||||
@close="closeOverlay"
|
||||
@change="onOverlayChange"
|
||||
@watched-change="onWatchedChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user