release: bump version to 0.49.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,16 @@ function formatInfoCardValue(field, rawValue) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function escapeInfoCardHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}[char]));
|
||||
}
|
||||
|
||||
function getNewsSummaryText(data) {
|
||||
return (data?.summary || data?.title || '').trim() || '暂无摘要';
|
||||
}
|
||||
@@ -130,6 +140,10 @@ function renderMobileDetailContent(type, config, data) {
|
||||
renderMobileNewsCardContent(content, data);
|
||||
return;
|
||||
}
|
||||
if (config.className === 'compute_unresolved') {
|
||||
renderComputeCenterUnresolvedContent(content, data);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
@@ -182,6 +196,11 @@ function ensureMobileDetailsListener() {
|
||||
}
|
||||
|
||||
function renderDefaultCardContent(content, config, data) {
|
||||
if (config.className === 'compute_unresolved') {
|
||||
renderComputeCenterUnresolvedContent(content, data);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
const value = formatInfoCardValue(field, data[field.key]);
|
||||
@@ -198,7 +217,472 @@ function renderDefaultCardContent(content, config, data) {
|
||||
html += renderVesselEnrichmentSection(data?.enrichment);
|
||||
}
|
||||
|
||||
const collectContext = buildLocationCollectContext(config, data);
|
||||
if (collectContext) {
|
||||
html += renderLocationCollectSection(collectContext);
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
|
||||
if (collectContext) {
|
||||
bindLocationCollectControls(content, collectContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve which entity (if any) supports the shared "collect candidate location"
|
||||
// flow on this info card. Returns a context object the renderer / binder both
|
||||
// consume, or null when the entity has no location-collection backend.
|
||||
function buildLocationCollectContext(config, data) {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
if (config.className === 'supercomputer' || config.className === 'gpu_cluster') {
|
||||
if (!data.source_id) return null;
|
||||
return {
|
||||
entityType: 'compute_center',
|
||||
entityId: data.source_id,
|
||||
data,
|
||||
needsConfirmation:
|
||||
data.needs_confirmation === true
|
||||
|| data.location_source === 'nominatim_online_geocode',
|
||||
collect: async () => {
|
||||
const mod = await import('./compute-centers.js');
|
||||
return mod.collectComputeCenterLocation(data.source_id, {
|
||||
name: data.name,
|
||||
operator: data.operator,
|
||||
site: data.site || data.metadata?.site,
|
||||
organization: data.metadata?.organization,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
source: data.source,
|
||||
record_id: data.id,
|
||||
});
|
||||
},
|
||||
save: async (candidate) => {
|
||||
const mod = await import('./compute-centers.js');
|
||||
return mod.saveComputeCenterLocation(data.source_id, candidate, {
|
||||
name: data.name,
|
||||
operator: data.operator,
|
||||
site: data.site || data.metadata?.site,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
source: data.source,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
if (config.className === 'bgp') {
|
||||
const collectorId = data.collector;
|
||||
if (!collectorId) return null;
|
||||
return {
|
||||
entityType: 'bgp_collector',
|
||||
entityId: collectorId,
|
||||
data,
|
||||
needsConfirmation:
|
||||
data.needs_confirmation === true
|
||||
|| (data.location_source && data.location_source !== 'source_coordinates'),
|
||||
collect: async () => {
|
||||
const mod = await import('./compute-centers.js');
|
||||
return mod.collectLocationCandidates(
|
||||
`/api/v1/bgp/collectors/${encodeURIComponent(collectorId)}/collect-location`,
|
||||
{
|
||||
site: data.site || data.matched_location_name,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
operator: data.operator,
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderLocationCollectSection(context) {
|
||||
const buttonLabel = context.needsConfirmation
|
||||
? '重新自动采集坐标'
|
||||
: '自动采集坐标候选';
|
||||
return `
|
||||
<div class="info-card-compute-collect" data-collect-entity-id="${context.entityId}" data-collect-entity-type="${context.entityType}">
|
||||
<button type="button" class="info-card-compute-collect-button" data-collect-action="run">
|
||||
<span class="material-symbols-rounded" aria-hidden="true">explore</span>
|
||||
<span>${buttonLabel}</span>
|
||||
</button>
|
||||
<div class="info-card-compute-collect-status" data-collect-status></div>
|
||||
<div class="info-card-compute-collect-candidates" data-collect-candidates></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function bindLocationCollectControls(content, context) {
|
||||
const collectRoot = content.querySelector('[data-collect-entity-id]');
|
||||
if (!collectRoot) return;
|
||||
const button = collectRoot.querySelector('[data-collect-action="run"]');
|
||||
const statusEl = collectRoot.querySelector('[data-collect-status]');
|
||||
const candidatesEl = collectRoot.querySelector('[data-collect-candidates]');
|
||||
if (!button) return;
|
||||
button.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
button.disabled = true;
|
||||
statusEl.textContent = '正在采集坐标候选...';
|
||||
candidatesEl.innerHTML = '';
|
||||
try {
|
||||
const result = await context.collect();
|
||||
if (!result?.success) {
|
||||
statusEl.textContent = `未能采集到坐标:${result?.failure_reason || '未知原因'}`;
|
||||
return;
|
||||
}
|
||||
const candidates = Array.isArray(result.candidates) ? result.candidates : [];
|
||||
statusEl.textContent = `共找到 ${candidates.length} 个候选位置`;
|
||||
candidatesEl.innerHTML = candidates
|
||||
.slice(0, 5)
|
||||
.map((candidate, index) => renderCollectCandidateRow(candidate, index === 0))
|
||||
.join('');
|
||||
bindCandidatePreviewButtons(candidatesEl, context);
|
||||
bindCandidateSaveButtons(candidatesEl, context, statusEl);
|
||||
} catch (error) {
|
||||
console.error('collect-location failed', error);
|
||||
statusEl.textContent = `采集失败:${error?.message || error}`;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}, { once: false });
|
||||
}
|
||||
|
||||
function renderCollectCandidateRow(candidate, isBest) {
|
||||
const precisionLabel = {
|
||||
precise: '精确',
|
||||
site: '站点',
|
||||
city: '城市',
|
||||
}[candidate.precision] || candidate.precision || '未知';
|
||||
const confidence = Number.isFinite(Number(candidate.confidence))
|
||||
? `${Math.round(Number(candidate.confidence) * 100)}%`
|
||||
: '-';
|
||||
const candidateJson = JSON.stringify(candidate).replace(/"/g, '"');
|
||||
return `
|
||||
<div class="info-card-compute-candidate ${isBest ? 'is-best' : ''}">
|
||||
<div class="info-card-compute-candidate-line">
|
||||
<span class="info-card-compute-candidate-name">${candidate.matched_location_name || candidate.display_name || '候选'}</span>
|
||||
<span class="info-card-compute-candidate-precision">${precisionLabel}</span>
|
||||
</div>
|
||||
<div class="info-card-compute-candidate-line">
|
||||
<span class="info-card-compute-candidate-source">${candidate.source}</span>
|
||||
<span class="info-card-compute-candidate-confidence">置信 ${confidence}</span>
|
||||
</div>
|
||||
<div class="info-card-compute-candidate-line">
|
||||
<span class="info-card-compute-candidate-coords">${Number(candidate.latitude).toFixed(4)}, ${Number(candidate.longitude).toFixed(4)}</span>
|
||||
<button type="button" class="info-card-compute-candidate-preview" data-preview-candidate
|
||||
data-lat="${candidate.latitude}" data-lon="${candidate.longitude}"
|
||||
data-candidate-json="${candidateJson}">预览</button>
|
||||
<button type="button" class="info-card-compute-candidate-preview" data-save-candidate
|
||||
data-candidate-json="${candidateJson}">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getUnresolvedComputeCenterContext(item) {
|
||||
const metadata = item?.metadata && typeof item.metadata === 'object'
|
||||
? item.metadata
|
||||
: {};
|
||||
return {
|
||||
sourceId: item?.source_id || item?.id || '',
|
||||
recordId: item?.id || item?.record_id || '',
|
||||
name: item?.name || item?.title || '未命名算力中心',
|
||||
operator: item?.operator || item?.vendor || metadata.operator || '',
|
||||
site: item?.site || metadata.site || metadata.organization || '',
|
||||
city: item?.city || metadata.city || '',
|
||||
country: item?.country || metadata.country || '',
|
||||
source: item?.source || metadata.source || '',
|
||||
};
|
||||
}
|
||||
|
||||
function renderComputeCenterUnresolvedContent(content, data) {
|
||||
const items = Array.isArray(data?.items) ? data.items : [];
|
||||
if (!items.length) {
|
||||
content.innerHTML = `
|
||||
<div class="info-card-unresolved-empty">
|
||||
当前没有待定位算力中心
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = items
|
||||
.map((item, index) => {
|
||||
const context = getUnresolvedComputeCenterContext(item);
|
||||
const contextJson = JSON.stringify(context).replace(/"/g, '"');
|
||||
const meta = [context.site || context.operator, context.city, context.country]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || '缺少可用地址字段';
|
||||
return `
|
||||
<div class="info-card-unresolved-item" data-unresolved-item>
|
||||
<div class="info-card-unresolved-main">
|
||||
<div class="info-card-unresolved-index">${index + 1}</div>
|
||||
<div class="info-card-unresolved-copy">
|
||||
<div class="info-card-unresolved-name">${escapeInfoCardHtml(context.name)}</div>
|
||||
<div class="info-card-unresolved-meta">${escapeInfoCardHtml(meta)}</div>
|
||||
</div>
|
||||
<button type="button" class="info-card-compute-candidate-preview" data-unresolved-collect
|
||||
data-context-json="${contextJson}">采集</button>
|
||||
</div>
|
||||
<div class="info-card-compute-collect-status" data-unresolved-status></div>
|
||||
<div class="info-card-compute-collect-candidates" data-unresolved-candidates></div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="info-card-unresolved-summary">
|
||||
<span data-unresolved-summary-text>${items.length} 个算力中心没有可信坐标</span>
|
||||
<button type="button" class="info-card-compute-candidate-preview info-card-unresolved-adopt" data-unresolved-adopt-all>
|
||||
一键采用
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-card-compute-collect-status" data-unresolved-batch-status></div>
|
||||
<div class="info-card-unresolved-list">
|
||||
${rows}
|
||||
</div>
|
||||
`;
|
||||
bindComputeCenterUnresolvedControls(content);
|
||||
}
|
||||
|
||||
function updateUnresolvedSummary(content) {
|
||||
const remainingCount = content.querySelectorAll('[data-unresolved-item]').length;
|
||||
const summaryText = content.querySelector('[data-unresolved-summary-text]');
|
||||
if (summaryText) {
|
||||
summaryText.textContent = remainingCount > 0
|
||||
? `${remainingCount} 个算力中心没有可信坐标`
|
||||
: '当前没有待定位算力中心';
|
||||
}
|
||||
const adoptAllButton = content.querySelector('[data-unresolved-adopt-all]');
|
||||
if (adoptAllButton instanceof HTMLButtonElement) {
|
||||
adoptAllButton.hidden = remainingCount <= 0;
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:compute-center-unresolved-count-change', {
|
||||
detail: { unresolvedCount: remainingCount },
|
||||
}),
|
||||
);
|
||||
return remainingCount;
|
||||
}
|
||||
|
||||
function renumberUnresolvedItems(content) {
|
||||
content.querySelectorAll('[data-unresolved-item]').forEach((item, index) => {
|
||||
const indexEl = item.querySelector('.info-card-unresolved-index');
|
||||
if (indexEl) indexEl.textContent = String(index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
function removeResolvedUnresolvedItem(content, itemRoot) {
|
||||
itemRoot?.remove();
|
||||
renumberUnresolvedItems(content);
|
||||
return updateUnresolvedSummary(content);
|
||||
}
|
||||
|
||||
async function collectUnresolvedComputeCenterCandidates(context) {
|
||||
const mod = await import('./compute-centers.js');
|
||||
const result = await mod.collectComputeCenterLocation(context.sourceId, {
|
||||
name: context.name,
|
||||
operator: context.operator,
|
||||
site: context.site,
|
||||
city: context.city,
|
||||
country: context.country,
|
||||
source: context.source,
|
||||
record_id: context.recordId,
|
||||
});
|
||||
return {
|
||||
mod,
|
||||
result,
|
||||
candidates: Array.isArray(result?.candidates) ? result.candidates : [],
|
||||
};
|
||||
}
|
||||
|
||||
function getBestLocationCandidate(candidates) {
|
||||
return candidates
|
||||
.filter((candidate) => (
|
||||
Number.isFinite(Number(candidate?.latitude))
|
||||
&& Number.isFinite(Number(candidate?.longitude))
|
||||
))
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const confidenceA = Number.isFinite(Number(a?.confidence))
|
||||
? Number(a.confidence)
|
||||
: -1;
|
||||
const confidenceB = Number.isFinite(Number(b?.confidence))
|
||||
? Number(b.confidence)
|
||||
: -1;
|
||||
return confidenceB - confidenceA;
|
||||
})[0] || null;
|
||||
}
|
||||
|
||||
function bindCandidatePreviewButtons(container, context) {
|
||||
container.querySelectorAll('[data-preview-candidate]').forEach((el) => {
|
||||
el.addEventListener('click', (clickEvt) => {
|
||||
clickEvt.stopPropagation();
|
||||
const lat = Number(el.dataset.lat);
|
||||
const lon = Number(el.dataset.lon);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:preview-location-candidate', {
|
||||
detail: {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
entityType: context.entityType,
|
||||
entityId: context.entityId,
|
||||
candidate: JSON.parse(el.dataset.candidateJson || '{}'),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindCandidateSaveButtons(container, context, statusEl) {
|
||||
container.querySelectorAll('[data-save-candidate]').forEach((el) => {
|
||||
el.addEventListener('click', async (clickEvt) => {
|
||||
clickEvt.stopPropagation();
|
||||
if (typeof context.save !== 'function') return;
|
||||
const candidate = JSON.parse(el.dataset.candidateJson || '{}');
|
||||
el.disabled = true;
|
||||
if (statusEl) statusEl.textContent = '正在保存所选坐标...';
|
||||
try {
|
||||
await context.save(candidate);
|
||||
if (statusEl) statusEl.textContent = '坐标已保存,正在刷新图层...';
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:compute-center-location-saved', {
|
||||
detail: {
|
||||
entityType: context.entityType,
|
||||
entityId: context.entityId,
|
||||
candidate,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('save compute-center location failed', error);
|
||||
if (statusEl) statusEl.textContent = `保存失败:${error?.message || error}`;
|
||||
} finally {
|
||||
el.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindComputeCenterUnresolvedControls(content) {
|
||||
content.querySelectorAll('[data-unresolved-collect]').forEach((button) => {
|
||||
button.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
const itemRoot = button.closest('[data-unresolved-item]');
|
||||
const statusEl = itemRoot?.querySelector('[data-unresolved-status]');
|
||||
const candidatesEl = itemRoot?.querySelector('[data-unresolved-candidates]');
|
||||
const context = JSON.parse(button.dataset.contextJson || '{}');
|
||||
if (!context.sourceId || !statusEl || !candidatesEl) return;
|
||||
|
||||
button.disabled = true;
|
||||
statusEl.textContent = '正在采集坐标候选...';
|
||||
candidatesEl.innerHTML = '';
|
||||
try {
|
||||
const { mod, result, candidates } = await collectUnresolvedComputeCenterCandidates(context);
|
||||
if (!result?.success) {
|
||||
statusEl.textContent = `未能采集到坐标:${result?.failure_reason || '未知原因'}`;
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = `共找到 ${candidates.length} 个候选位置`;
|
||||
candidatesEl.innerHTML = candidates
|
||||
.slice(0, 5)
|
||||
.map((candidate, index) => renderCollectCandidateRow(candidate, index === 0))
|
||||
.join('');
|
||||
const actionContext = {
|
||||
entityType: 'compute_center',
|
||||
entityId: context.sourceId,
|
||||
save: (candidate) => mod.saveComputeCenterLocation(context.sourceId, candidate, context),
|
||||
};
|
||||
bindCandidatePreviewButtons(candidatesEl, actionContext);
|
||||
bindCandidateSaveButtons(candidatesEl, actionContext, statusEl);
|
||||
} catch (error) {
|
||||
console.error('collect unresolved compute-center location failed', error);
|
||||
statusEl.textContent = `采集失败:${error?.message || error}`;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const adoptAllButton = content.querySelector('[data-unresolved-adopt-all]');
|
||||
if (adoptAllButton instanceof HTMLButtonElement) {
|
||||
adoptAllButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
const statusEl = content.querySelector('[data-unresolved-batch-status]');
|
||||
const buttons = Array.from(content.querySelectorAll('button'));
|
||||
const pendingItems = Array.from(content.querySelectorAll('[data-unresolved-item]'))
|
||||
.map((itemRoot) => {
|
||||
const collectButton = itemRoot.querySelector('[data-unresolved-collect]');
|
||||
const context = JSON.parse(collectButton?.dataset.contextJson || '{}');
|
||||
return { itemRoot, context };
|
||||
})
|
||||
.filter(({ context }) => context.sourceId);
|
||||
|
||||
if (!pendingItems.length) return;
|
||||
buttons.forEach((button) => { button.disabled = true; });
|
||||
|
||||
let savedCount = 0;
|
||||
let missedCount = 0;
|
||||
try {
|
||||
for (const [index, { itemRoot, context }] of pendingItems.entries()) {
|
||||
const itemStatusEl = itemRoot.querySelector('[data-unresolved-status]');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `正在采用最高置信候选 ${index + 1}/${pendingItems.length}...`;
|
||||
}
|
||||
try {
|
||||
const { mod, result, candidates } = await collectUnresolvedComputeCenterCandidates(context);
|
||||
if (!result?.success) {
|
||||
if (itemStatusEl) {
|
||||
itemStatusEl.textContent = `未找到可采用候选:${result?.failure_reason || '未知原因'}`;
|
||||
}
|
||||
missedCount += 1;
|
||||
continue;
|
||||
}
|
||||
const bestCandidate = getBestLocationCandidate(candidates);
|
||||
if (!bestCandidate) {
|
||||
if (itemStatusEl) {
|
||||
itemStatusEl.textContent = '未找到包含有效经纬度的候选';
|
||||
}
|
||||
missedCount += 1;
|
||||
continue;
|
||||
}
|
||||
await mod.saveComputeCenterLocation(context.sourceId, bestCandidate, context);
|
||||
savedCount += 1;
|
||||
removeResolvedUnresolvedItem(content, itemRoot);
|
||||
} catch (error) {
|
||||
console.error('adopt unresolved compute-center location failed', error);
|
||||
if (itemStatusEl) {
|
||||
itemStatusEl.textContent = `一键采用失败:${error?.message || error}`;
|
||||
}
|
||||
missedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = savedCount > 0
|
||||
? `已采用 ${savedCount} 个最高置信候选${missedCount ? `,${missedCount} 个仍需手动处理` : ''}`
|
||||
: `${missedCount} 个都没有可自动采用的候选,需要手动处理`;
|
||||
}
|
||||
if (savedCount > 0) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:compute-center-location-saved', {
|
||||
detail: {
|
||||
entityType: 'compute_center',
|
||||
entityId: 'batch',
|
||||
savedCount,
|
||||
missedCount,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
buttons.forEach((button) => { button.disabled = false; });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getFieldSourceLabel(data, fieldKey) {
|
||||
@@ -283,6 +767,7 @@ function getMobilePopupTitle(type, data) {
|
||||
case 'bgp': return data.anomaly_type || 'BGP事件';
|
||||
case 'news': return data.title || '新闻事件';
|
||||
case 'bgp_collector': return data.collector || 'BGP观测站';
|
||||
case 'compute_center_unresolved': return '待定位算力中心';
|
||||
case 'supercomputer': return data.name || '超算';
|
||||
case 'gpu_cluster': return data.name || 'GPU集群';
|
||||
case 'vessel': return data.name || '船只';
|
||||
@@ -298,6 +783,7 @@ function getMobilePopupSubtitle(type, data) {
|
||||
case 'bgp': return data.severity || 'BGP路由异常';
|
||||
case 'news': return getNewsSummaryPreview(data, 30) || '态势新闻';
|
||||
case 'bgp_collector': return data.location || 'BGP观测站';
|
||||
case 'compute_center_unresolved': return `${data?.totalCount || 0} 个待定位`;
|
||||
case 'supercomputer': return data.country || '超级计算机';
|
||||
case 'gpu_cluster': return data.country || 'GPU集群';
|
||||
case 'vessel': return data.vessel_type || 'AIS 船只';
|
||||
@@ -593,6 +1079,12 @@ const CARD_CONFIG = {
|
||||
{ key: 'status', label: '状态' }
|
||||
]
|
||||
},
|
||||
compute_center_unresolved: {
|
||||
icon: '📍',
|
||||
title: '待定位算力中心',
|
||||
className: 'compute_unresolved',
|
||||
fields: []
|
||||
},
|
||||
supercomputer: {
|
||||
icon: '🖥️',
|
||||
title: '超算中心详情',
|
||||
@@ -609,6 +1101,13 @@ const CARD_CONFIG = {
|
||||
{ key: 'country', label: '国家' },
|
||||
{ key: 'city', label: '城市' },
|
||||
{ key: 'location_precision_label', label: '位置精度' },
|
||||
{ key: 'location_source_label', label: '位置来源' },
|
||||
{ key: 'location_confidence', label: '位置置信度' },
|
||||
{ key: 'location_status_label', label: '核验状态' },
|
||||
{ key: 'estimated_reason', label: '解析依据' },
|
||||
{ key: 'location_source_note', label: '位置来源说明' },
|
||||
{ key: 'matched_location_name', label: '匹配的位置名称' },
|
||||
{ key: 'location_verified_at', label: '位置核验时间' },
|
||||
{ key: 'source', label: '来源' },
|
||||
{ key: 'updated_at', label: '更新时间' }
|
||||
]
|
||||
@@ -628,6 +1127,13 @@ const CARD_CONFIG = {
|
||||
{ key: 'country', label: '国家' },
|
||||
{ key: 'city', label: '城市' },
|
||||
{ key: 'location_precision_label', label: '位置精度' },
|
||||
{ key: 'location_source_label', label: '位置来源' },
|
||||
{ key: 'location_confidence', label: '位置置信度' },
|
||||
{ key: 'location_status_label', label: '核验状态' },
|
||||
{ key: 'estimated_reason', label: '解析依据' },
|
||||
{ key: 'location_source_note', label: '位置来源说明' },
|
||||
{ key: 'matched_location_name', label: '匹配的位置名称' },
|
||||
{ key: 'location_verified_at', label: '位置核验时间' },
|
||||
{ key: 'source', label: '来源' },
|
||||
{ key: 'updated_at', label: '更新时间' }
|
||||
]
|
||||
@@ -875,6 +1381,7 @@ function showPanel(x, y, options = {}) {
|
||||
const panel = getPanel();
|
||||
if (!panel) return;
|
||||
panel.classList.toggle('hud-panel-info--anchor-stable', options.anchorStable === true);
|
||||
panel.dataset.sticky = options.sticky === true ? 'true' : 'false';
|
||||
panel.removeAttribute('hidden');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
if (x != null && y != null) positionPanel(panel, x, y, options);
|
||||
@@ -896,6 +1403,7 @@ function hidePanel() {
|
||||
if (panel) {
|
||||
panel.classList.remove('is-visible');
|
||||
panel.classList.remove('hud-panel-info--anchor-stable');
|
||||
delete panel.dataset.sticky;
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
panel.setAttribute('hidden', '');
|
||||
}
|
||||
@@ -915,6 +1423,10 @@ export function setInfoCardNoBorder(noBorder = true) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isInfoCardSticky() {
|
||||
return getPanel()?.dataset.sticky === 'true';
|
||||
}
|
||||
|
||||
export function showInfoCard(type, data, options = {}) {
|
||||
const config = CARD_CONFIG[type];
|
||||
if (!config) {
|
||||
|
||||
Reference in New Issue
Block a user