feat(ui): adopt low-profile callout capsule for spotlight tour in update guide

This commit is contained in:
DemonGatanjieu
2026-09-19 15:59:23 +08:00
parent 63769026fb
commit bea49f08d3
3 changed files with 61 additions and 46 deletions
+2
View File
@@ -33,6 +33,7 @@ export const i18n = {
shortcutDropRelease: "松开,从快捷栏移除",
shortcutBarAnchorTip: "固定入口,不可移动",
updateGuideStartTour: "🎯 界面按键遮罩导览",
updateGuideTourBanner: "💡 想在主界面实地体验?点击开启按键遮罩导览 ›",
updateGuideNavigationTitle: "悬停,看懂每个入口",
updateGuideNavigationBody: "左下角的按钮保持原来的大小。鼠标悬停或用键盘选中时,图标会切换成文字;稍作停留可以查看完整用途。依次为工具箱、扫描、模型医生、节点助手、素材库和设置。",
updateGuideTransferTitle: "导入导出,统一到工具箱",
@@ -1249,6 +1250,7 @@ export const i18n = {
shortcutDropRelease: "Release to remove from shortcuts",
shortcutBarAnchorTip: "Fixed anchor, cannot be moved",
updateGuideStartTour: "🎯 Spotlight Tour",
updateGuideTourBanner: "💡 Prefer to see them in action? Start Spotlight Tour ›",
updateGuideNavigationTitle: "Hover to identify each action",
updateGuideNavigationBody: "The lower-left buttons keep their compact size. Hover or focus with the keyboard to replace an icon with a short label; pause for its full description. The actions are Toolbox, Scan, Model Doctor, Node Assistant, Material Library and Settings.",
updateGuideTransferTitle: "Workflow sharing lives in Toolbox",
+44 -37
View File
@@ -254,47 +254,54 @@ export function ensureTourStyles() {
background: #1d4ed8;
}
.anomalous-btn-tour {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
color: #ffffff !important;
border: 1px solid rgba(147, 197, 253, 0.7) !important;
border-radius: 6px !important;
padding: 7px 16px !important;
font-size: 12px !important;
font-weight: 600 !important;
cursor: pointer !important;
box-shadow: 0 0 12px rgba(37, 99, 235, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1) !important;
animation: anomalous-tour-btn-pulse 2.6s infinite ease-in-out !important;
letter-spacing: 0.2px !important;
}
@keyframes anomalous-tour-btn-pulse {
0%, 100% {
box-shadow: 0 0 10px rgba(37, 99, 235, 0.4), 0 0 0 0 rgba(59, 130, 246, 0.4);
}
50% {
box-shadow: 0 0 18px rgba(37, 99, 235, 0.75), 0 0 0 3px rgba(59, 130, 246, 0.25);
}
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: #2563eb;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 7px 16px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.anomalous-btn-tour:hover {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
border-color: #ffffff !important;
color: #ffffff !important;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.7), 0 4px 14px rgba(0, 0, 0, 0.35) !important;
transform: translateY(-1px) !important;
animation: none !important;
background: #1d4ed8;
border-color: rgba(255, 255, 255, 0.4);
}
.anomalous-btn-tour:active {
transform: translateY(0) !important;
box-shadow: 0 0 8px rgba(37, 99, 235, 0.5) !important;
.anomalous-update-guide-tour-banner {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin: 2px auto 14px auto;
padding: 6px 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
color: #94a3b8;
font-size: 11.5px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
max-width: fit-content;
text-align: center;
user-select: none;
letter-spacing: 0.2px;
}
@media (prefers-reduced-motion: reduce) {
.anomalous-btn-tour {
animation: none !important;
}
.anomalous-update-guide-tour-banner:hover {
background: rgba(59, 130, 246, 0.12);
border-color: rgba(96, 165, 250, 0.4);
color: #93c5fd;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.anomalous-update-guide-tour-banner:active {
transform: translateY(0);
background: rgba(59, 130, 246, 0.2);
}
`;
(document.head || document.body)?.appendChild(style);
+15 -9
View File
@@ -64,25 +64,31 @@ export function showUpdateGuide(owner, { force = false, guide = CURRENT_UPDATE_G
title.id = 'anomalous-update-guide-title';
const body = text(dialog, 'p', '');
body.id = 'anomalous-update-guide-body';
const tourBanner = text(dialog, 'button', t('updateGuideTourBanner') || '💡 想在主界面实地体验?点击开启按键遮罩导览 ›', 'anomalous-update-guide-tour-banner');
tourBanner.type = 'button';
tourBanner.id = 'anomalous-update-guide-tour-btn';
tourBanner.onclick = () => {
closeUpdateGuide(owner, true);
startSpotlightTour(owner);
};
const progress = text(dialog, 'p', '', 'anomalous-update-guide-progress');
progress.setAttribute('aria-live', 'polite');
const footer = text(dialog, 'div', '', 'anomalous-update-guide-footer');
const skip = text(footer, 'button', t('updateGuideSkip'), 'anomalous-btn-ghost');
const startTour = text(footer, 'button', t('updateGuideStartTour') || '🎯 界面按键遮罩导览', 'anomalous-btn-tour');
startTour.id = 'anomalous-update-guide-tour-btn';
startTour.onclick = () => {
closeUpdateGuide(owner, true);
startSpotlightTour(owner);
};
const back = text(footer, 'button', t('updateGuideBack'), 'anomalous-btn-ghost');
const next = text(footer, 'button', '', 'anomalous-btn-primary');
for (const button of [skip, startTour, back, next]) button.type = 'button';
const navGroup = text(footer, 'div', '', 'anomalous-update-guide-nav-group');
navGroup.style.display = 'flex';
navGroup.style.gap = '8px';
navGroup.style.alignItems = 'center';
const back = text(navGroup, 'button', t('updateGuideBack'), 'anomalous-btn-ghost');
const next = text(navGroup, 'button', '', 'anomalous-btn-primary');
for (const button of [skip, back, next, tourBanner]) button.type = 'button';
let index = 0;
const render = () => {
const step = guide.steps[index];
icon.textContent = step.icon;
title.textContent = t(step.titleKey);
body.textContent = t(step.bodyKey);
tourBanner.textContent = t('updateGuideTourBanner') || '💡 想在主界面实地体验?点击开启按键遮罩导览 ›';
progress.textContent = t('updateGuideProgress', { current: index + 1, total: guide.steps.length });
back.disabled = index === 0;
next.textContent = t(index === guide.steps.length - 1 ? 'updateGuideDone' : 'updateGuideNext');