migration: preserve frontend shell pages and styles
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
window.XiaobaiPageModules.register("dragon_tiger", ["dragonView"], {
|
||||
enter: ["loadDragonTiger"],
|
||||
});
|
||||
|
||||
/* PRESERVATION-SOURCE-BEGIN app.js:2660-3028 */
|
||||
function selectDragonViewMode(mode) {
|
||||
state.dragonViewMode = mode === "profiles" ? "profiles" : "daily";
|
||||
document.querySelectorAll("[data-dragon-view-mode]").forEach((button) => {
|
||||
const active = button.dataset.dragonViewMode === state.dragonViewMode;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
});
|
||||
if (state.dragonViewMode === "profiles") {
|
||||
document.querySelector("#dragonDailyContent").hidden = true;
|
||||
document.querySelector("#dragonEmptyState").hidden = true;
|
||||
document.querySelector("#dragonProfilesContent").hidden = false;
|
||||
if (state.hotMoneyProfiles) renderHotMoneyProfiles();
|
||||
else loadHotMoneyProfiles();
|
||||
} else {
|
||||
document.querySelector("#dragonProfilesContent").hidden = true;
|
||||
if (state.dragonTiger) renderDragonTiger();
|
||||
else loadDragonTiger();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHotMoneyProfiles(force = false) {
|
||||
if (!force && state.hotMoneyProfiles) {
|
||||
renderHotMoneyProfiles();
|
||||
return;
|
||||
}
|
||||
setStatus("正在加载游资档案");
|
||||
try {
|
||||
const query = new URLSearchParams();
|
||||
if (force) query.set("force", "1");
|
||||
const suffix = query.size ? `?${query}` : "";
|
||||
state.hotMoneyProfiles = await apiRequest(`/api/dragon-tiger/profiles${suffix}`);
|
||||
renderHotMoneyProfiles();
|
||||
const count = number(state.hotMoneyProfiles.summary?.profile_count);
|
||||
setStatus(`游资档案已加载 · 共 ${count} 位`);
|
||||
} catch (error) {
|
||||
showToast(error.message || "游资档案加载失败");
|
||||
setStatus("游资档案加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
function renderHotMoneyProfiles() {
|
||||
const payload = state.hotMoneyProfiles;
|
||||
if (!payload) return;
|
||||
const profiles = payload.profiles || [];
|
||||
const summary = payload.summary || {};
|
||||
const query = state.hotMoneyProfileQuery;
|
||||
const visible = profiles.filter((profile) => {
|
||||
if (!query) return true;
|
||||
return [profile.name, profile.description, ...(profile.organizations || [])]
|
||||
.join(" ")
|
||||
.toLocaleLowerCase("zh-CN")
|
||||
.includes(query);
|
||||
});
|
||||
if (!visible.some((profile) => profile.id === state.selectedHotMoneyProfileId)) {
|
||||
state.selectedHotMoneyProfileId = visible[0]?.id || "";
|
||||
}
|
||||
const selected = visible.find((profile) => profile.id === state.selectedHotMoneyProfileId) || null;
|
||||
|
||||
setText("dragonDateLabel", `收录 ${number(summary.profile_count)} 位`);
|
||||
setText("hotMoneyProfileResultCount", query ? `${visible.length} / ${profiles.length} 位` : `${profiles.length} 位`);
|
||||
document.querySelector("#hotMoneyProfileSummary").innerHTML = [
|
||||
["收录游资", number(summary.profile_count)],
|
||||
["已有简介", number(summary.described_count)],
|
||||
["关联席位", number(summary.organization_count)],
|
||||
].map(([label, value]) => `<span><small>${label}</small><strong>${value}</strong></span>`).join("");
|
||||
|
||||
const list = document.querySelector("#hotMoneyProfileList");
|
||||
list.innerHTML = visible.length ? visible.map((profile, index) => `
|
||||
<button class="hot-money-profile-row-v2 ${profile.id === state.selectedHotMoneyProfileId ? "selected" : ""}"
|
||||
type="button" role="option" aria-selected="${profile.id === state.selectedHotMoneyProfileId}"
|
||||
data-hot-money-profile="${escapeHtml(profile.id)}">
|
||||
<span class="hot-money-profile-index-v2">${String(index + 1).padStart(2, "0")}</span>
|
||||
<span class="hot-money-profile-monogram-v2">${escapeHtml(profile.name.slice(0, 2))}</span>
|
||||
<span class="hot-money-profile-row-copy-v2">
|
||||
<strong>${escapeHtml(profile.name)}</strong>
|
||||
<small>${escapeHtml(profile.description || "暂未收录简介")}</small>
|
||||
</span>
|
||||
<span class="hot-money-profile-seat-count-v2">${number(profile.organization_count)} 席</span>
|
||||
</button>`).join("") : `
|
||||
<div class="hot-money-profile-list-empty-v2">
|
||||
<i data-lucide="search-x" aria-hidden="true"></i>
|
||||
<span>${profiles.length ? "没有符合条件的游资档案" : "游资名录暂不可用"}</span>
|
||||
</div>`;
|
||||
|
||||
const detail = document.querySelector("#hotMoneyProfileDetail");
|
||||
if (!selected) {
|
||||
detail.innerHTML = `
|
||||
<div class="hot-money-profile-empty-v2">
|
||||
<i data-lucide="contact" aria-hidden="true"></i>
|
||||
<strong>${profiles.length ? "选择一位游资查看档案" : "暂无可展示的游资档案"}</strong>
|
||||
</div>`;
|
||||
} else {
|
||||
const organizations = selected.organizations || [];
|
||||
detail.innerHTML = `
|
||||
<header class="hot-money-profile-detail-head-v2">
|
||||
<span class="hot-money-profile-avatar-v2">${escapeHtml(selected.name.slice(0, 2))}</span>
|
||||
<div>
|
||||
<small>游资档案</small>
|
||||
<h3>${escapeHtml(selected.name)}</h3>
|
||||
<span>${organizations.length ? `关联 ${organizations.length} 个公开席位` : "暂无关联席位"}</span>
|
||||
</div>
|
||||
</header>
|
||||
<section class="hot-money-profile-section-v2">
|
||||
<h4>人物简介</h4>
|
||||
<p class="${selected.description ? "" : "is-empty"}">${escapeHtml(selected.description || "名录暂未收录该游资的公开简介。")}</p>
|
||||
</section>
|
||||
<section class="hot-money-profile-section-v2 hot-money-profile-org-section-v2">
|
||||
<div class="hot-money-profile-section-title-v2">
|
||||
<h4>关联营业部</h4>
|
||||
<span>${organizations.length} 个</span>
|
||||
</div>
|
||||
<div class="hot-money-profile-organizations-v2">
|
||||
${organizations.length ? organizations.map((organization) => `
|
||||
<span><i data-lucide="building-2" aria-hidden="true"></i>${escapeHtml(organization)}</span>
|
||||
`).join("") : '<p class="is-empty">名录暂未收录关联营业部。</p>'}
|
||||
</div>
|
||||
</section>
|
||||
${payload.meta?.notice ? `<p class="hot-money-profile-notice-v2">${escapeHtml(payload.meta.notice)}</p>` : ""}`;
|
||||
}
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
async function loadDragonTiger(force = false) {
|
||||
const requestedDate = elements.tradeDate.value;
|
||||
if (
|
||||
!force
|
||||
&& ["success", "empty", "partial", "unavailable"].includes(state.dragonTiger?.meta?.status)
|
||||
&& (state.dragonTiger?.meta?.requested_date || state.dragonTiger?.meta?.trade_date) === requestedDate
|
||||
) {
|
||||
renderDragonTiger();
|
||||
return;
|
||||
}
|
||||
setStatus("正在加载龙虎榜");
|
||||
try {
|
||||
const query = new URLSearchParams({ trade_date: requestedDate });
|
||||
if (force) query.set("force", "1");
|
||||
const payload = await apiRequest(`/api/dragon-tiger?${query}`);
|
||||
state.dragonTiger = payload;
|
||||
renderDragonTiger();
|
||||
const statusLabel = payload.meta.status === "error"
|
||||
? "龙虎榜数据暂不可用"
|
||||
: payload.meta.status === "empty"
|
||||
? "当日暂无公开游资明细"
|
||||
: payload.meta.status === "partial"
|
||||
? "当日有龙虎榜,暂无命名游资明细"
|
||||
: payload.meta.status === "unavailable" ? "龙虎榜数据暂不可用" : "龙虎榜明细";
|
||||
setStatus(`${statusLabel} · 龙虎榜已加载`);
|
||||
} catch (error) {
|
||||
showToast(error.message || "龙虎榜加载失败");
|
||||
setStatus("龙虎榜加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
function renderDragonTiger() {
|
||||
const payload = state.dragonTiger;
|
||||
if (!payload) return;
|
||||
const summary = payload.summary || {};
|
||||
if (state.dragonViewMode === "daily") setText("dragonDateLabel", `数据日期 ${payload.meta.trade_date}`);
|
||||
const status = payload.meta?.status || "empty";
|
||||
const hasRecognizedTraders = (payload.traders || []).some((item) => item.identity_type === "trader" && item.recognized !== false);
|
||||
const showEmptyState = !hasRecognizedTraders
|
||||
&& !(payload.unclassified_seats || []).length
|
||||
&& ["empty", "error", "unavailable"].includes(status);
|
||||
const dailyVisible = state.dragonViewMode === "daily";
|
||||
document.querySelector("#dragonProfilesContent").hidden = dailyVisible;
|
||||
document.querySelector("#dragonEmptyState").hidden = !dailyVisible || !showEmptyState;
|
||||
document.querySelector("#dragonDailyContent").hidden = !dailyVisible || showEmptyState;
|
||||
if (showEmptyState) {
|
||||
const unavailable = ["error", "unavailable"].includes(status);
|
||||
setText("dragonEmptyTitle", unavailable ? "龙虎榜数据暂不可用" : `${payload.meta?.trade_date || "该交易日"} 暂无龙虎榜明细`);
|
||||
setText("dragonEmptyDescription", unavailable
|
||||
? "当前数据暂未完成更新,可稍后重新检查或查看前一交易日。"
|
||||
: "龙虎榜明细通常在交易日盘后陆续披露,可稍后刷新或查看前一交易日。");
|
||||
}
|
||||
document.querySelector("#dragonSummary").innerHTML = [
|
||||
["上榜游资", `${number(summary.trader_count)} 位`, ""],
|
||||
["操作明细", `${number(summary.operation_count)} 条`, ""],
|
||||
["席位净买入", formatMoneyMillion(summary.seat_net_buy_million), changeClass(summary.seat_net_buy_million)],
|
||||
["活跃股票", `${number(summary.active_stock_count)} 只`, ""],
|
||||
].map(([label, value, className]) => `<div class="dragon-metric"><span>${label}</span><strong class="${className}">${value}</strong></div>`).join("");
|
||||
|
||||
renderDragonTraderList();
|
||||
renderUnclassifiedSeats();
|
||||
}
|
||||
|
||||
function renderDragonTraderList() {
|
||||
const payload = state.dragonTiger;
|
||||
if (!payload) return;
|
||||
let traders = [...(payload.traders || [])].filter((item) => item.identity_type === "trader" && item.recognized !== false);
|
||||
if (state.dragonFilter === "buy") traders = traders.filter((item) => number(item.net_buy_million) > 0);
|
||||
if (state.dragonFilter === "sell") traders = traders.filter((item) => number(item.net_buy_million) < 0);
|
||||
if (state.dragonFilter === "unclassified") traders = [];
|
||||
if (state.dragonQuery) {
|
||||
traders = traders.filter((item) => {
|
||||
const searchable = [
|
||||
item.name,
|
||||
...(item.operations || []).flatMap((operation) => [operation.code, operation.name, operation.seat_name]),
|
||||
].join(" ").toLowerCase();
|
||||
return searchable.includes(state.dragonQuery);
|
||||
});
|
||||
}
|
||||
|
||||
const container = document.querySelector("#dragonTraderList");
|
||||
let emptyMessage = "没有符合当前条件的游资操作";
|
||||
if (!Array.isArray(payload.traders)) emptyMessage = "龙虎榜数据格式暂不可用,请稍后重试";
|
||||
else if (["error", "unavailable"].includes(payload.meta?.status)) emptyMessage = "龙虎榜数据暂不可用,请稍后重试";
|
||||
else if (payload.meta?.status === "empty") emptyMessage = "该交易日暂无游资每日明细";
|
||||
else if (payload.meta?.status === "partial") emptyMessage = `当日有 ${number(payload.summary?.official_stock_count)} 只股票上榜,但暂无可识别的游资明细`;
|
||||
if (!traders.some((item) => item.id === state.selectedDragonTraderId)) {
|
||||
state.selectedDragonTraderId = traders[0]?.id || "";
|
||||
}
|
||||
const cardMarkup = traders.map((trader, index) => {
|
||||
const description = trader.description || `${number(trader.stock_count)} 只股票,${number(trader.operation_count)} 笔操作`;
|
||||
return `
|
||||
<article class="dragon-trader-card dealing ${trader.id === state.selectedDragonTraderId ? "selected" : ""}" data-dragon-card="${escapeHtml(trader.id)}" aria-hidden="true" style="--deal-delay:${Math.min(index * 38, 650)}ms">
|
||||
<span class="dragon-card-rank">${String(index + 1).padStart(2, "0")}</span>
|
||||
<span class="dragon-card-monogram">${escapeHtml(trader.name.slice(0, 2))}</span>
|
||||
<span class="dragon-card-copy"><strong>${escapeHtml(trader.name)}</strong><q title="${escapeHtml(description)}">${escapeHtml(description)}</q></span>
|
||||
<span class="dragon-card-stats"><small>${number(trader.stock_count)} 股 · ${number(trader.operation_count)} 笔</small><b class="${changeClass(trader.net_buy_million)}">${formatMoneyMillion(trader.net_buy_million)}</b></span>
|
||||
</article>`;
|
||||
}).join("");
|
||||
const hitZoneMarkup = traders.map((trader) => `
|
||||
<button type="button" class="dragon-card-hit-zone" data-dragon-trader="${escapeHtml(trader.id)}" aria-label="查看 ${escapeHtml(trader.name)} 当日操作" aria-pressed="${trader.id === state.selectedDragonTraderId}"></button>
|
||||
`).join("");
|
||||
container.innerHTML = traders.length
|
||||
? `${cardMarkup}<div class="dragon-card-hit-layer">${hitZoneMarkup}</div>`
|
||||
: emptyStateHtml(state.dragonFilter === "unclassified" ? "待归类席位请在下方管理" : emptyMessage, { className: "dragon-empty" });
|
||||
container.querySelectorAll("[data-dragon-card]").forEach((card) => {
|
||||
card.addEventListener("animationend", () => card.classList.remove("dealing"), { once: true });
|
||||
});
|
||||
container.querySelectorAll("[data-dragon-trader]").forEach((hitZone) => {
|
||||
const setHovered = (hovered) => {
|
||||
container.querySelector(`[data-dragon-card="${CSS.escape(hitZone.dataset.dragonTrader)}"]`)?.classList.toggle("hovered", hovered);
|
||||
};
|
||||
hitZone.addEventListener("pointerenter", () => setHovered(true));
|
||||
hitZone.addEventListener("pointerleave", () => setHovered(false));
|
||||
hitZone.addEventListener("focus", () => setHovered(true));
|
||||
hitZone.addEventListener("blur", () => setHovered(false));
|
||||
hitZone.addEventListener("click", () => {
|
||||
state.selectedDragonTraderId = hitZone.dataset.dragonTrader;
|
||||
container.querySelectorAll("[data-dragon-card]").forEach((card) => {
|
||||
card.classList.toggle("selected", card.dataset.dragonCard === state.selectedDragonTraderId);
|
||||
});
|
||||
container.querySelectorAll("[data-dragon-trader]").forEach((item) => {
|
||||
item.setAttribute("aria-pressed", String(item.dataset.dragonTrader === state.selectedDragonTraderId));
|
||||
});
|
||||
renderDragonTraderDetail(traders.find((item) => item.id === state.selectedDragonTraderId));
|
||||
});
|
||||
});
|
||||
requestAnimationFrame(() => layoutDragonCards(container));
|
||||
renderDragonTraderDetail(traders.find((item) => item.id === state.selectedDragonTraderId));
|
||||
}
|
||||
|
||||
function layoutDragonCards(container = document.querySelector("#dragonTraderList")) {
|
||||
if (!container) return;
|
||||
const cards = [...container.querySelectorAll(".dragon-trader-card")];
|
||||
const hitZones = [...container.querySelectorAll(".dragon-card-hit-zone")];
|
||||
if (!cards.length) return;
|
||||
const compact = window.innerWidth <= 720;
|
||||
const cardWidth = compact ? 148 : 176;
|
||||
const available = Math.max(cardWidth, container.clientWidth - (compact ? 30 : 72));
|
||||
const spread = Math.min(available - cardWidth, compact ? 310 : 1050);
|
||||
const step = cards.length > 1 ? Math.min(cardWidth + 14, spread / (cards.length - 1)) : 0;
|
||||
const center = (cards.length - 1) / 2;
|
||||
container.style.setProperty("--dragon-card-width", `${cardWidth}px`);
|
||||
cards.forEach((card, index) => {
|
||||
const x = (index - center) * step;
|
||||
card.style.setProperty("--card-x", `${x.toFixed(2)}px`);
|
||||
card.style.setProperty("--card-rotation", "0deg");
|
||||
card.style.setProperty("--card-y", "0px");
|
||||
card.style.zIndex = String(index + 1);
|
||||
const hitZone = hitZones[index];
|
||||
if (hitZone) {
|
||||
const zoneWidth = index === cards.length - 1 ? cardWidth : Math.max(18, step);
|
||||
hitZone.style.left = `calc(50% + ${(x - cardWidth / 2).toFixed(2)}px)`;
|
||||
hitZone.style.width = `${zoneWidth.toFixed(2)}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderDragonTraderDetail(trader) {
|
||||
const container = document.querySelector("#dragonTraderDetail");
|
||||
if (!trader) {
|
||||
container.hidden = true;
|
||||
renderEmptyState(container, "选择一位游资查看操作明细", { className: "dragon-empty" });
|
||||
return;
|
||||
}
|
||||
container.hidden = false;
|
||||
container.innerHTML = `
|
||||
<header class="dragon-detail-header">
|
||||
<div><span>当日操作明细</span><h3>${escapeHtml(trader.name)}</h3><p>${escapeHtml(trader.description || "按当日公开龙虎榜席位汇总")}</p></div>
|
||||
<dl><div><dt>买入</dt><dd class="up">${formatMoneyMillion(trader.buy_million)}</dd></div><div><dt>卖出</dt><dd class="down">${formatMoneyMillion(trader.sell_million)}</dd></div><div><dt>净额</dt><dd class="${changeClass(trader.net_buy_million)}">${formatMoneyMillion(trader.net_buy_million)}</dd></div></dl>
|
||||
</header>
|
||||
<div class="trader-operations table-frame tbl-wrap">
|
||||
<table class="data-table tbl dragon-operation-table">
|
||||
<colgroup><col class="dragon-col-index"><col class="dragon-col-stock"><col class="dragon-col-direction"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-seat"><col class="dragon-col-reason"></colgroup>
|
||||
<thead><tr><th class="row-number num">序号</th><th>股票</th><th>方向</th><th class="number num">涨幅(%)</th><th class="number num">买入(百万)</th><th class="number num">卖出(百万)</th><th class="number num">净额(百万)</th><th>关联席位</th><th class="reason-column">标签 / 上榜原因</th></tr></thead>
|
||||
<tbody>${(trader.operations || []).map((operation, index) => `
|
||||
<tr data-code="${escapeHtml(operation.code)}">
|
||||
<td class="row-number num">${index + 1}</td>
|
||||
<td><strong class="sname">${escapeHtml(operation.name)}</strong><span class="scode">${escapeHtml(operation.code)}</span></td>
|
||||
<td><span class="direction-label ${changeClass(operation.net_buy_million)}">${escapeHtml(operation.direction)}</span></td>
|
||||
<td class="number num ${operation.change == null ? "" : changeClass(operation.change)}">${operation.change == null ? "" : signed(operation.change)}</td>
|
||||
<td class="number num">${operation.buy_million == null ? "" : formatNumber(operation.buy_million, 2)}</td>
|
||||
<td class="number num">${operation.sell_million == null ? "" : formatNumber(operation.sell_million, 2)}</td>
|
||||
<td class="number num ${operation.net_buy_million == null ? "" : changeClass(operation.net_buy_million)}">${operation.net_buy_million == null ? "" : signed(operation.net_buy_million)}</td>
|
||||
<td class="seat-cell" title="${escapeHtml(operation.seat_name)}">${escapeHtml(operation.seat_name)}</td>
|
||||
<td class="reason-column" title="${escapeHtml([operation.tag, operation.reason].filter((item) => item && item !== "--").join(" · "))}">${escapeHtml(operation.tag && operation.tag !== "--" ? operation.tag : operation.reason && operation.reason !== "--" ? operation.reason : "")}</td>
|
||||
</tr>`).join("")}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
bindStockRows(container);
|
||||
markAutoSortableHeaders(container);
|
||||
}
|
||||
|
||||
function renderUnclassifiedSeats() {
|
||||
const seats = state.dragonTiger?.unclassified_seats || [];
|
||||
const canManage = state.user?.role === "admin";
|
||||
document.querySelector("#dragonUnclassifiedSection").hidden = !canManage || seats.length === 0;
|
||||
document.querySelector("#dragonUnclassifiedFilter").hidden = !canManage || seats.length === 0;
|
||||
if (!seats.length && state.dragonFilter === "unclassified") {
|
||||
state.dragonFilter = "all";
|
||||
document.querySelectorAll("[data-dragon-filter]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.dragonFilter === "all");
|
||||
});
|
||||
renderDragonTraderList();
|
||||
}
|
||||
setText("unclassifiedCount", `${seats.length} 个`);
|
||||
const list = document.querySelector("#unclassifiedSeatList");
|
||||
list.innerHTML = seats.map((seat, index) => `
|
||||
<form class="unclassified-seat-row" data-unclassified-index="${index}">
|
||||
<span class="unclassified-seat-name" title="${escapeHtml(seat.seat_name)}">${escapeHtml(seat.seat_name)}</span>
|
||||
<span class="unclassified-seat-stats">${number(seat.operation_count)} 笔 · ${number(seat.stock_count)} 股</span>
|
||||
<strong class="${changeClass(seat.net_buy_million)}">${formatMoneyMillion(seat.net_buy_million)}</strong>
|
||||
<input type="text" maxlength="50" placeholder="输入游资名" aria-label="${escapeHtml(seat.seat_name)}的游资名" required>
|
||||
<button class="button" type="submit">归类</button>
|
||||
</form>
|
||||
`).join("") || emptyStateHtml("当前席位均已归类");
|
||||
list.querySelectorAll(".unclassified-seat-row").forEach((form) => {
|
||||
form.addEventListener("submit", saveSeatAlias);
|
||||
});
|
||||
}
|
||||
|
||||
function dragonIdentityLabel(type) {
|
||||
return { trader: "游资", institution: "机构", channel: "通道", unclassified: "待归类" }[type] || "席位";
|
||||
}
|
||||
|
||||
async function saveSeatAlias(event) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const seat = state.dragonTiger?.unclassified_seats?.[number(form.dataset.unclassifiedIndex)];
|
||||
const alias = form.querySelector("input").value.trim();
|
||||
if (!seat || !alias) {
|
||||
showToast("请输入游资名");
|
||||
return;
|
||||
}
|
||||
const button = form.querySelector("button");
|
||||
button.disabled = true;
|
||||
try {
|
||||
await apiRequest("/api/seat-aliases", "POST", { seat_name: seat.seat_name, alias });
|
||||
state.dragonTiger = null;
|
||||
await loadDragonTiger();
|
||||
showToast(`已将席位归类为 ${alias}`);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* PRESERVATION-SOURCE-END app.js:2660-3028 */
|
||||
Reference in New Issue
Block a user