feat(chat): show_html 卡片工具栏改为 ⋯ 菜单并新增全屏预览
This commit is contained in:
parent
79b916f44c
commit
2db77fe663
@ -505,42 +505,309 @@ function escapeHtml(input: string) {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindShowHtmlRefreshControl(wrapper: HTMLElement, onRefresh: () => void) {
|
/**
|
||||||
|
* show_html 卡片全屏预览载荷。
|
||||||
|
* - js="on" 卡片:直接复用已注入 CSP/守卫的 srcdoc,沙箱允许脚本
|
||||||
|
* - js="off" 卡片:用 sanitize 后的 HTML 现场构建 srcdoc,沙箱禁脚本(与卡片内语义一致)
|
||||||
|
*/
|
||||||
|
interface ShowHtmlFullscreenPayload {
|
||||||
|
srcdoc: string;
|
||||||
|
allowScripts: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShowHtmlCardControls {
|
||||||
|
onRefresh: () => void;
|
||||||
|
getFullscreenPayload: () => ShowHtmlFullscreenPayload | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHOW_HTML_CONTROLS_KEY = '__showHtmlCardControls';
|
||||||
|
|
||||||
|
function getShowHtmlCardControls(wrapper: HTMLElement): ShowHtmlCardControls | null {
|
||||||
|
return ((wrapper as any)[SHOW_HTML_CONTROLS_KEY] as ShowHtmlCardControls | undefined) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerShowHtmlCardRefresh(wrapper: HTMLElement, btn: HTMLElement | null) {
|
||||||
|
const controls = getShowHtmlCardControls(wrapper);
|
||||||
|
if (!controls) return;
|
||||||
|
markShowTagDrawingActive(1200);
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.remove('is-refreshing');
|
||||||
|
// 触发重放动画
|
||||||
|
void btn.offsetWidth;
|
||||||
|
btn.classList.add('is-refreshing');
|
||||||
|
window.setTimeout(() => btn.classList.remove('is-refreshing'), 720);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
controls.onRefresh();
|
||||||
|
} catch (error) {
|
||||||
|
debugShowHtmlLog('refresh:error', {
|
||||||
|
message: error instanceof Error ? error.message : String(error || '')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 卡片 ⋯ 菜单(全局 fixed 单例,同一时间只存在一个) =====
|
||||||
|
let showHtmlCardMenuEl: HTMLElement | null = null;
|
||||||
|
let showHtmlCardMenuAnchor: HTMLElement | null = null;
|
||||||
|
let showHtmlCardMenuEventsBound = false;
|
||||||
|
|
||||||
|
function closeShowHtmlCardMenu() {
|
||||||
|
if (showHtmlCardMenuEl) {
|
||||||
|
showHtmlCardMenuEl.remove();
|
||||||
|
showHtmlCardMenuEl = null;
|
||||||
|
showHtmlCardMenuAnchor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindShowHtmlCardMenuGlobalEvents() {
|
||||||
|
if (showHtmlCardMenuEventsBound || typeof document === 'undefined') return;
|
||||||
|
showHtmlCardMenuEventsBound = true;
|
||||||
|
document.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event) => {
|
||||||
|
if (!showHtmlCardMenuEl) return;
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
if (target && showHtmlCardMenuEl.contains(target)) return;
|
||||||
|
// 锚点按钮自身走 toggle 逻辑,不在全局监听里关闭
|
||||||
|
if (target && showHtmlCardMenuAnchor?.contains(target)) return;
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
'keydown',
|
||||||
|
(event) => {
|
||||||
|
if (event.key === 'Escape' && showHtmlCardMenuEl) {
|
||||||
|
event.stopPropagation();
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
window.addEventListener('resize', closeShowHtmlCardMenu);
|
||||||
|
// 聊天区滚动时菜单位置即失效,直接关闭(capture 以捕获任意滚动容器)
|
||||||
|
document.addEventListener(
|
||||||
|
'scroll',
|
||||||
|
() => {
|
||||||
|
if (showHtmlCardMenuEl) closeShowHtmlCardMenu();
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildShowHtmlCardMenuItem(label: string): HTMLButtonElement {
|
||||||
|
const item = document.createElement('button');
|
||||||
|
item.type = 'button';
|
||||||
|
item.className = 'show-html-card-menu__item';
|
||||||
|
item.textContent = label;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShowHtmlCardMenu(anchor: HTMLElement, wrapper: HTMLElement) {
|
||||||
|
bindShowHtmlCardMenuGlobalEvents();
|
||||||
|
if (showHtmlCardMenuEl && showHtmlCardMenuAnchor === anchor) {
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
|
||||||
|
const menu = document.createElement('div');
|
||||||
|
menu.className = 'show-html-card-menu';
|
||||||
|
menu.setAttribute('role', 'menu');
|
||||||
|
|
||||||
|
const refreshItem = buildShowHtmlCardMenuItem('刷新');
|
||||||
|
refreshItem.onclick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
triggerShowHtmlCardRefresh(wrapper, anchor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fullscreenItem = buildShowHtmlCardMenuItem('全屏');
|
||||||
|
fullscreenItem.onclick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const controls = getShowHtmlCardControls(wrapper);
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
const payload = controls?.getFullscreenPayload?.();
|
||||||
|
if (!payload || !payload.srcdoc) return;
|
||||||
|
openShowHtmlFullscreen(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.appendChild(refreshItem);
|
||||||
|
menu.appendChild(fullscreenItem);
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
|
||||||
|
// 定位:按钮下方、右对齐,视口边缘留 8px 安全距离
|
||||||
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
const menuWidth = menu.offsetWidth;
|
||||||
|
let left = Math.round(rect.right - menuWidth);
|
||||||
|
left = Math.max(8, Math.min(left, window.innerWidth - menuWidth - 8));
|
||||||
|
menu.style.left = `${left}px`;
|
||||||
|
menu.style.top = `${Math.round(rect.bottom + 6)}px`;
|
||||||
|
|
||||||
|
showHtmlCardMenuEl = menu;
|
||||||
|
showHtmlCardMenuAnchor = anchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== show_html 全屏预览(应用内 overlay,复用沙箱 srcdoc,不走路由) =====
|
||||||
|
interface ShowHtmlFullscreenState {
|
||||||
|
root: HTMLElement;
|
||||||
|
iframe: HTMLIFrameElement;
|
||||||
|
payload: ShowHtmlFullscreenPayload | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHOW_HTML_FULLSCREEN_EMPTY_SRCDOC =
|
||||||
|
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
||||||
|
|
||||||
|
let showHtmlFullscreenState: ShowHtmlFullscreenState | null = null;
|
||||||
|
let showHtmlFullscreenEventsBound = false;
|
||||||
|
|
||||||
|
function isShowHtmlFullscreenOpen(): boolean {
|
||||||
|
return !!showHtmlFullscreenState?.root.classList.contains('is-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindShowHtmlFullscreenGlobalEvents() {
|
||||||
|
if (showHtmlFullscreenEventsBound || typeof document === 'undefined') return;
|
||||||
|
showHtmlFullscreenEventsBound = true;
|
||||||
|
// 桌面端 Esc 退出;移动端没有 Esc,依赖顶栏 ✕ 按钮(触屏主路径)
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key !== 'Escape' || !isShowHtmlFullscreenOpen()) return;
|
||||||
|
event.stopPropagation();
|
||||||
|
closeShowHtmlFullscreen();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureShowHtmlFullscreenDom(): ShowHtmlFullscreenState {
|
||||||
|
if (showHtmlFullscreenState) return showHtmlFullscreenState;
|
||||||
|
bindShowHtmlFullscreenGlobalEvents();
|
||||||
|
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.className = 'show-html-fullscreen';
|
||||||
|
root.setAttribute('role', 'dialog');
|
||||||
|
root.setAttribute('aria-modal', 'true');
|
||||||
|
root.setAttribute('aria-label', '卡片全屏预览');
|
||||||
|
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'show-html-fullscreen__bar';
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.className = 'show-html-fullscreen__title';
|
||||||
|
title.textContent = '全屏预览';
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'show-html-fullscreen__actions';
|
||||||
|
|
||||||
|
const refreshBtn = document.createElement('button');
|
||||||
|
refreshBtn.type = 'button';
|
||||||
|
refreshBtn.className = 'show-html-fullscreen__btn';
|
||||||
|
refreshBtn.title = '刷新';
|
||||||
|
refreshBtn.setAttribute('aria-label', '刷新');
|
||||||
|
refreshBtn.textContent = '↻';
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.type = 'button';
|
||||||
|
closeBtn.className = 'show-html-fullscreen__btn show-html-fullscreen__btn--close';
|
||||||
|
closeBtn.title = '关闭(Esc)';
|
||||||
|
closeBtn.setAttribute('aria-label', '关闭全屏预览');
|
||||||
|
closeBtn.textContent = '✕';
|
||||||
|
|
||||||
|
actions.appendChild(refreshBtn);
|
||||||
|
actions.appendChild(closeBtn);
|
||||||
|
bar.appendChild(title);
|
||||||
|
bar.appendChild(actions);
|
||||||
|
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'show-html-fullscreen__body';
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.className = 'show-html-fullscreen__iframe';
|
||||||
|
iframe.setAttribute('referrerpolicy', 'no-referrer');
|
||||||
|
// 与内联卡片一致:overlay 用 CSS fixed 占满视口,不需要浏览器 Fullscreen API
|
||||||
|
iframe.setAttribute('allow', "fullscreen 'none'");
|
||||||
|
body.appendChild(iframe);
|
||||||
|
|
||||||
|
root.appendChild(bar);
|
||||||
|
root.appendChild(body);
|
||||||
|
|
||||||
|
refreshBtn.onclick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const state = showHtmlFullscreenState;
|
||||||
|
if (!state?.payload) return;
|
||||||
|
// 与内联卡片刷新同策略:先挂空文档再重挂,强制 iframe 内页面重载
|
||||||
|
const keep = state.payload.srcdoc;
|
||||||
|
state.iframe.srcdoc = SHOW_HTML_FULLSCREEN_EMPTY_SRCDOC;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (showHtmlFullscreenState?.payload) {
|
||||||
|
showHtmlFullscreenState.iframe.srcdoc = keep;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
debugShowHtmlLog('fullscreen:refresh', { srcdocLength: keep.length });
|
||||||
|
};
|
||||||
|
closeBtn.onclick = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
closeShowHtmlFullscreen();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(root);
|
||||||
|
showHtmlFullscreenState = { root, iframe, payload: null };
|
||||||
|
return showHtmlFullscreenState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openShowHtmlFullscreen(payload: ShowHtmlFullscreenPayload) {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const state = ensureShowHtmlFullscreenDom();
|
||||||
|
state.payload = payload;
|
||||||
|
// js=off 卡片内容已 sanitize,全屏时连脚本执行权也不给,语义与卡片内一致
|
||||||
|
state.iframe.setAttribute('sandbox', payload.allowScripts ? 'allow-scripts' : '');
|
||||||
|
state.iframe.srcdoc = payload.srcdoc;
|
||||||
|
state.root.classList.add('is-open');
|
||||||
|
document.documentElement.classList.add('show-html-fullscreen-open');
|
||||||
|
debugShowHtmlLog('fullscreen:open', {
|
||||||
|
allowScripts: payload.allowScripts,
|
||||||
|
srcdocLength: payload.srcdoc.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeShowHtmlFullscreen() {
|
||||||
|
const state = showHtmlFullscreenState;
|
||||||
|
if (!state || !isShowHtmlFullscreenOpen()) return;
|
||||||
|
state.root.classList.remove('is-open');
|
||||||
|
document.documentElement.classList.remove('show-html-fullscreen-open');
|
||||||
|
state.payload = null;
|
||||||
|
// 清空 srcdoc:停掉 iframe 内脚本/定时器并释放内存
|
||||||
|
state.iframe.srcdoc = SHOW_HTML_FULLSCREEN_EMPTY_SRCDOC;
|
||||||
|
debugShowHtmlLog('fullscreen:close', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 show_html 卡片右上角工具栏(⋯ 菜单:刷新 / 全屏)。
|
||||||
|
* 渲染器每次重渲染都会调用:最新回调挂在 wrapper 上,按钮与菜单只创建一次。
|
||||||
|
*/
|
||||||
|
function bindShowHtmlCardControls(wrapper: HTMLElement, controls: ShowHtmlCardControls) {
|
||||||
|
(wrapper as any)[SHOW_HTML_CONTROLS_KEY] = controls;
|
||||||
let toolbar = wrapper.querySelector(':scope > .chat-inline-card__toolbar') as HTMLElement | null;
|
let toolbar = wrapper.querySelector(':scope > .chat-inline-card__toolbar') as HTMLElement | null;
|
||||||
if (!toolbar) {
|
if (!toolbar) {
|
||||||
toolbar = document.createElement('div');
|
toolbar = document.createElement('div');
|
||||||
toolbar.className = 'chat-inline-card__toolbar';
|
toolbar.className = 'chat-inline-card__toolbar';
|
||||||
wrapper.appendChild(toolbar);
|
wrapper.appendChild(toolbar);
|
||||||
}
|
}
|
||||||
let refreshBtn = toolbar.querySelector(
|
let menuBtn = toolbar.querySelector(
|
||||||
':scope > .chat-inline-card__refresh-btn'
|
':scope > .chat-inline-card__menu-btn'
|
||||||
) as HTMLButtonElement | null;
|
) as HTMLButtonElement | null;
|
||||||
if (!refreshBtn) {
|
if (!menuBtn) {
|
||||||
refreshBtn = document.createElement('button');
|
menuBtn = document.createElement('button');
|
||||||
refreshBtn.type = 'button';
|
menuBtn.type = 'button';
|
||||||
refreshBtn.className = 'chat-inline-card__refresh-btn';
|
menuBtn.className = 'chat-inline-card__menu-btn';
|
||||||
refreshBtn.title = '刷新卡片';
|
menuBtn.title = '卡片操作';
|
||||||
refreshBtn.setAttribute('aria-label', '刷新卡片');
|
menuBtn.setAttribute('aria-label', '卡片操作');
|
||||||
refreshBtn.textContent = '↻';
|
menuBtn.setAttribute('aria-haspopup', 'menu');
|
||||||
toolbar.appendChild(refreshBtn);
|
menuBtn.textContent = '⋯';
|
||||||
|
toolbar.appendChild(menuBtn);
|
||||||
}
|
}
|
||||||
refreshBtn.onclick = (event) => {
|
menuBtn.onclick = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
markShowTagDrawingActive(1200);
|
openShowHtmlCardMenu(menuBtn!, wrapper);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -787,21 +1054,29 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
};
|
};
|
||||||
showHtmlJsIframeRenderByPath.set(pathKey, persistent);
|
showHtmlJsIframeRenderByPath.set(pathKey, persistent);
|
||||||
}
|
}
|
||||||
bindShowHtmlRefreshControl(persistent.wrapper, () => {
|
bindShowHtmlCardControls(persistent.wrapper, {
|
||||||
// 强制重新挂载同一份 srcdoc,触发 iframe 内页面重载
|
onRefresh: () => {
|
||||||
const keep = persistent?.srcdoc || srcdoc;
|
// 强制重新挂载同一份 srcdoc,触发 iframe 内页面重载
|
||||||
persistent.iframe.srcdoc =
|
const keep = persistent?.srcdoc || srcdoc;
|
||||||
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
persistent.iframe.srcdoc =
|
||||||
requestAnimationFrame(() => {
|
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
||||||
if (persistent?.iframe) {
|
requestAnimationFrame(() => {
|
||||||
persistent.iframe.srcdoc = keep;
|
if (persistent?.iframe) {
|
||||||
}
|
persistent.iframe.srcdoc = keep;
|
||||||
});
|
}
|
||||||
debugShowHtmlLog('refresh:iframe', {
|
});
|
||||||
pathKey,
|
debugShowHtmlLog('refresh:iframe', {
|
||||||
ratioKey: persistent?.ratioKey || ratioKey,
|
pathKey,
|
||||||
srcdocLength: keep.length
|
ratioKey: persistent?.ratioKey || ratioKey,
|
||||||
});
|
srcdocLength: keep.length
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 全屏直接复用当前 srcdoc(已含 CSP meta 与守卫脚本),沙箱保持 allow-scripts
|
||||||
|
getFullscreenPayload: () => {
|
||||||
|
const current = persistent?.srcdoc || '';
|
||||||
|
if (!current) return null;
|
||||||
|
return { srcdoc: current, allowScripts: true };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const needsUpdate =
|
const needsUpdate =
|
||||||
persistent.encoded !== effectiveEncoded ||
|
persistent.encoded !== effectiveEncoded ||
|
||||||
@ -933,19 +1208,27 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
};
|
};
|
||||||
showHtmlPersistentRenderByPath.set(pathKey, persistent);
|
showHtmlPersistentRenderByPath.set(pathKey, persistent);
|
||||||
}
|
}
|
||||||
bindShowHtmlRefreshControl(persistent.wrapper, () => {
|
bindShowHtmlCardControls(persistent.wrapper, {
|
||||||
const keep = persistent?.safeHtml || '';
|
onRefresh: () => {
|
||||||
persistent.root.innerHTML = '';
|
const keep = persistent?.safeHtml || '';
|
||||||
requestAnimationFrame(() => {
|
persistent.root.innerHTML = '';
|
||||||
if (persistent?.root) {
|
requestAnimationFrame(() => {
|
||||||
persistent.root.innerHTML = keep;
|
if (persistent?.root) {
|
||||||
}
|
persistent.root.innerHTML = keep;
|
||||||
});
|
}
|
||||||
debugShowHtmlLog('refresh:shadow-root', {
|
});
|
||||||
pathKey,
|
debugShowHtmlLog('refresh:shadow-root', {
|
||||||
ratioKey: persistent?.ratioKey || ratioKey,
|
pathKey,
|
||||||
safeLength: keep.length
|
ratioKey: persistent?.ratioKey || ratioKey,
|
||||||
});
|
safeLength: keep.length
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// js=off 内容已 sanitize,全屏走禁脚本沙箱 iframe,渲染效果与 Shadow DOM 一致
|
||||||
|
getFullscreenPayload: () => {
|
||||||
|
const current = persistent?.safeHtml || '';
|
||||||
|
if (!current.trim()) return null;
|
||||||
|
return { srcdoc: buildShowHtmlIframeSrcdoc(current), allowScripts: false };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const needsUpdate =
|
const needsUpdate =
|
||||||
@ -1369,6 +1652,9 @@ export function teardownShowImageObserver() {
|
|||||||
showHtmlJsPendingRenderByPath.clear();
|
showHtmlJsPendingRenderByPath.clear();
|
||||||
showHtmlJsIframeRenderByPath.clear();
|
showHtmlJsIframeRenderByPath.clear();
|
||||||
showTagObservedContainer = null;
|
showTagObservedContainer = null;
|
||||||
|
// 菜单/全屏层是全局单例,卡片 Map 清空后其引用的 wrapper 已卸载,必须一并关闭
|
||||||
|
closeShowHtmlCardMenu();
|
||||||
|
closeShowHtmlFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateViewportHeightVar() {
|
function updateViewportHeightVar() {
|
||||||
|
|||||||
@ -1758,42 +1758,42 @@ body[data-theme='dark'] .more-icon {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-card__refresh-btn {
|
/* ⋯ 菜单按钮:纯文字风格,无边框无背景,hover 仅变色;保留 26px 点击热区 */
|
||||||
|
.chat-inline-card__menu-btn {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
border-radius: 999px;
|
border: 0;
|
||||||
border: 1px solid var(--border-default);
|
background: transparent;
|
||||||
background: color-mix(in srgb, var(--surface-card) 78%, transparent);
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
/* ⋯ 字形视觉重心偏下,上抬补偵使其看起来居中 */
|
||||||
|
padding-bottom: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition: color 0.18s ease;
|
||||||
color 0.18s ease,
|
|
||||||
border-color 0.18s ease,
|
|
||||||
background 0.18s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-card__refresh-btn:hover {
|
.chat-inline-card__menu-btn:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: var(--accent);
|
|
||||||
background: color-mix(in srgb, var(--highlight) 72%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-inline-card__refresh-btn.is-refreshing {
|
.chat-inline-card__menu-btn.is-refreshing {
|
||||||
animation: chat-inline-refresh-spin 0.6s linear 1;
|
animation: chat-inline-menu-pulse 0.6s ease 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes chat-inline-refresh-spin {
|
@keyframes chat-inline-menu-pulse {
|
||||||
from {
|
0% {
|
||||||
transform: rotate(0deg);
|
opacity: 1;
|
||||||
}
|
}
|
||||||
to {
|
40% {
|
||||||
transform: rotate(360deg);
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1818,6 +1818,148 @@ body[data-theme='dark'] .more-icon {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- show_html 卡片 ⋯ 菜单(全局 fixed 单例) ---------------- */
|
||||||
|
|
||||||
|
.show-html-card-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3050;
|
||||||
|
min-width: 112px;
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
animation: show-html-card-menu-in 0.16s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes show-html-card-menu-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-3px) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-card-menu__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-card-menu__item:hover {
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- show_html 全屏预览 overlay ---------------- */
|
||||||
|
|
||||||
|
/* 打开期间锁定底层页面滚动(聊天区滚动容器在 body 下) */
|
||||||
|
html.show-html-fullscreen-open,
|
||||||
|
html.show-html-fullscreen-open body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
/* 与聊天区同一套视口高度变量,规避移动端 100vh 地址栏问题 */
|
||||||
|
height: var(--app-viewport, 100vh);
|
||||||
|
z-index: 3000;
|
||||||
|
background: var(--surface-base);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen.is-open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__bar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
/* 手机刘海/状态栏安全区;移动端无法 Esc,关闭按钮必须永远可见可达 */
|
||||||
|
padding: calc(8px + env(safe-area-inset-top, 0px))
|
||||||
|
calc(10px + env(safe-area-inset-right, 0px)) 8px
|
||||||
|
calc(16px + env(safe-area-inset-left, 0px));
|
||||||
|
border-bottom: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与卡片 ⋯ 按钮一致的纯文字风格:无边框无背景,hover 仅变色 */
|
||||||
|
.show-html-fullscreen__btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__iframe {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端加大热区,保证触屏可可靠点中关闭按钮 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.show-html-fullscreen__btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 兼容旧类名(可逐步移除) */
|
/* 兼容旧类名(可逐步移除) */
|
||||||
.chat-inline-image__error {
|
.chat-inline-image__error {
|
||||||
@extend .chat-inline-card__error;
|
@extend .chat-inline-card__error;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user