From 3cd8bdabef4987817a001a8027e103af54594352 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 27 Apr 2026 01:42:24 +0800 Subject: [PATCH] feat(ui): add refresh button for showhtml cards --- static/src/app/bootstrap.ts | 76 +++++++++++++++++++ .../styles/components/chat/_chat-area.scss | 48 ++++++++++++ 2 files changed, 124 insertions(+) diff --git a/static/src/app/bootstrap.ts b/static/src/app/bootstrap.ts index 36addb7..fd02fca 100644 --- a/static/src/app/bootstrap.ts +++ b/static/src/app/bootstrap.ts @@ -380,6 +380,45 @@ function escapeHtml(input: string) { .replace(/'/g, '''); } +function bindShowHtmlRefreshControl(wrapper: HTMLElement, onRefresh: () => void) { + let toolbar = wrapper.querySelector(':scope > .chat-inline-card__toolbar') as HTMLElement | null; + if (!toolbar) { + toolbar = document.createElement('div'); + toolbar.className = 'chat-inline-card__toolbar'; + wrapper.appendChild(toolbar); + } + let refreshBtn = toolbar.querySelector( + ':scope > .chat-inline-card__refresh-btn' + ) as HTMLButtonElement | null; + if (!refreshBtn) { + refreshBtn = document.createElement('button'); + refreshBtn.type = 'button'; + refreshBtn.className = 'chat-inline-card__refresh-btn'; + refreshBtn.title = '刷新卡片'; + refreshBtn.setAttribute('aria-label', '刷新卡片'); + refreshBtn.textContent = '↻'; + toolbar.appendChild(refreshBtn); + } + refreshBtn.onclick = (event) => { + event.preventDefault(); + event.stopPropagation(); + markShowTagDrawingActive(1200); + refreshBtn!.classList.remove('is-refreshing'); + // 触发重放动画 + void refreshBtn!.offsetWidth; + refreshBtn!.classList.add('is-refreshing'); + try { + onRefresh(); + } catch (error) { + debugShowHtmlLog('refresh:error', { + message: error instanceof Error ? error.message : String(error || '') + }); + } finally { + window.setTimeout(() => refreshBtn?.classList.remove('is-refreshing'), 720); + } + }; +} + function renderShowImages(root: ParentNode | null = document) { if (!root) return; const debugMode = getShowImageDebugMode(); @@ -648,6 +687,24 @@ function renderShowImages(root: ParentNode | null = document) { }; showHtmlJsIframeRenderByPath.set(pathKey, persistent); } + bindShowHtmlRefreshControl(persistent.wrapper, () => { + // 强制重新挂载同一份 srcdoc,触发 iframe 内页面重载 + const keep = persistent?.srcdoc || srcdoc; + persistent.iframe.srcdoc = + '
'; + requestAnimationFrame(() => { + if (persistent?.iframe) { + persistent.iframe.srcdoc = keep; + } + }); + debugShowHtmlLog('refresh:iframe', { + pathKey, + ratioKey: persistent?.ratioKey || ratioKey, + widthPx: persistent?.widthPx || widthPx, + heightPx: persistent?.heightPx || heightPx, + srcdocLength: keep.length + }); + }); persistent.wrapper.style.width = `${widthPx}px`; persistent.iframe.style.height = `${heightPx}px`; const needsUpdate = @@ -784,6 +841,7 @@ function renderShowImages(root: ParentNode | null = document) { wrapper.appendChild(host); persistent = { encoded: '', + safeHtml: '', ratioKey: '1:1', widthPx: 0, heightPx: 0, @@ -793,6 +851,22 @@ function renderShowImages(root: ParentNode | null = document) { }; showHtmlPersistentRenderByPath.set(pathKey, persistent); } + bindShowHtmlRefreshControl(persistent.wrapper, () => { + const keep = persistent?.safeHtml || ''; + persistent.root.innerHTML = ''; + requestAnimationFrame(() => { + if (persistent?.root) { + persistent.root.innerHTML = keep; + } + }); + debugShowHtmlLog('refresh:shadow-root', { + pathKey, + ratioKey: persistent?.ratioKey || ratioKey, + widthPx: persistent?.widthPx || widthPx, + heightPx: persistent?.heightPx || heightPx, + safeLength: keep.length + }); + }); persistent.wrapper.style.width = `${widthPx}px`; persistent.host.style.height = `${heightPx}px`; @@ -804,6 +878,7 @@ function renderShowImages(root: ParentNode | null = document) { if (needsUpdate) { persistent.root.innerHTML = safeHtml; persistent.encoded = effectiveEncoded; + persistent.safeHtml = safeHtml; persistent.widthPx = widthPx; persistent.heightPx = heightPx; persistent.ratioKey = ratioKey; @@ -888,6 +963,7 @@ const showHtmlPersistentRenderByPath = new Map< string, { encoded: string, + safeHtml: string, ratioKey: string, widthPx: number, heightPx: number, diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss index 795b869..8a0eb36 100644 --- a/static/src/styles/components/chat/_chat-area.scss +++ b/static/src/styles/components/chat/_chat-area.scss @@ -965,6 +965,54 @@ body[data-theme='dark'] .more-icon { .chat-inline-card--html { display: block; + position: relative; +} + +.chat-inline-card__toolbar { + position: absolute; + top: 8px; + right: 8px; + z-index: 4; + display: flex; + align-items: center; + gap: 6px; + pointer-events: none; +} + +.chat-inline-card__refresh-btn { + pointer-events: auto; + width: 26px; + height: 26px; + border-radius: 999px; + border: 1px solid var(--claude-border); + background: color-mix(in srgb, var(--claude-card) 78%, transparent); + color: var(--claude-text-secondary); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 15px; + line-height: 1; + cursor: pointer; + transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease; +} + +.chat-inline-card__refresh-btn:hover { + color: var(--claude-text); + border-color: var(--claude-accent); + background: color-mix(in srgb, var(--claude-highlight) 72%, transparent); +} + +.chat-inline-card__refresh-btn.is-refreshing { + animation: chat-inline-refresh-spin 0.6s linear 1; +} + +@keyframes chat-inline-refresh-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } .chat-inline-html__host--pending {