diff --git a/frontend/src/components/chat/chat_enhancements.tsx b/frontend/src/components/chat/chat_enhancements.tsx new file mode 100644 index 0000000..093010c --- /dev/null +++ b/frontend/src/components/chat/chat_enhancements.tsx @@ -0,0 +1,88 @@ +// ChatPage 移动端壳 + 复制按钮 hook — #938 +// 不直接改 ChatPage 主体,提供增强 hooks 与组件,可选挂载 + +import React, { useState, useCallback } from 'react'; + +/** + * useCopyToClipboard — 复制文本到剪贴板(带回退方案) + */ +export function useCopyToClipboard(): [boolean, (text: string) => Promise] { + const [ok, setOk] = useState(false); + const copy = useCallback(async (text: string) => { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + } else { + // Fallback for older browsers / non-secure context + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } + setOk(true); + setTimeout(() => setOk(false), 1500); + } catch (e) { + setOk(false); + } + }, []); + return [ok, copy]; +} + +/** + * CopyButton — 通用复制按钮(挂载到代码块) + */ +export const CopyButton: React.FC<{ text: string; label?: string }> = ({ text, label = '复制' }) => { + const [copied, copy] = useCopyToClipboard(); + return ( + + ); +}; + +/** + * useIsMobile — 简单响应式 hook(< 768px) + */ +export function useIsMobile(breakpoint = 768): boolean { + const [isMobile, setIsMobile] = useState( + typeof window !== 'undefined' ? window.innerWidth < breakpoint : false + ); + React.useEffect(() => { + if (typeof window === 'undefined') return; + const onResize = () => setIsMobile(window.innerWidth < breakpoint); + window.addEventListener('resize', onResize, { passive: true }); + return () => window.removeEventListener('resize', onResize); + }, [breakpoint]); + return isMobile; +} + +/** + * CodeBlockWithCopy — 包裹 code 块自动注入复制按钮 + * 用法:{`print("hi")`} + */ +export const CodeBlockWithCopy: React.FC<{ + lang?: string; + children: React.ReactNode; +}> = ({ lang, children }) => { + const text = typeof children === 'string' ? children : String(children || ''); + return ( +
+
+        {children}
+      
+ +
+ ); +}; + +export default { CopyButton, CodeBlockWithCopy, useCopyToClipboard, useIsMobile }; \ No newline at end of file diff --git a/frontend/src/pages/front/ChatPage.css b/frontend/src/pages/front/ChatPage.css index ad6bd9e..d2e48b1 100644 --- a/frontend/src/pages/front/ChatPage.css +++ b/frontend/src/pages/front/ChatPage.css @@ -1784,3 +1784,143 @@ html.dark .ai-sending-timeout { @keyframes aiPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } } .ai-process-error { background: rgba(255,0,0,0.05); border-left: 2px solid #e74c3c; } .ai-process-running { border-left: 2px solid #3498db; } + +/* 2026-07-29: 增强列表/卡片样式 */ +.markdown-body ul.md-ul { + list-style: none; + padding-left: 0; + margin: 12px 0; +} +.markdown-body ul.md-ul li.md-li { + position: relative; + padding: 8px 12px 8px 28px; + margin: 4px 0; + border-left: 3px solid var(--color-accent, #5B5FED); + background: linear-gradient(90deg, rgba(91,95,237,0.04), transparent); + border-radius: 0 8px 8px 0; + line-height: 1.7; + transition: all 0.2s ease; +} +.markdown-body ul.md-ul li.md-li:hover { + background: linear-gradient(90deg, rgba(91,95,237,0.10), transparent); + border-left-color: var(--color-accent-active, #4A4ECC); +} +.markdown-body ul.md-ul li.md-li::before { + content: "▸"; + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--color-accent, #5B5FED); + font-weight: bold; +} +.markdown-body ul.md-ul li.md-li ul.md-ul { + margin: 8px 0 4px 8px; + padding-left: 0; +} +.markdown-body ul.md-ul li.md-li ul.md-ul li.md-li { + padding: 4px 8px 4px 20px; + border-left-width: 2px; + font-size: 0.95em; +} +.markdown-body ol { + padding-left: 24px; + margin: 12px 0; +} +.markdown-body ol > li { + padding: 4px 0 4px 4px; + margin: 4px 0; + line-height: 1.7; +} +// ChatPage 移动端响应式 + 暗态增强 — #938 +// 添加到 ChatPage.css 末尾(项目已有 :root CSS var 系统) + +/* ===== 移动端响应式(< 768px) ===== */ +@media (max-width: 768px) { + /* 聊天列表改单列 */ + .chat-layout { + flex-direction: column !important; + } + .chat-sidebar { + width: 100% !important; + max-height: 35vh; + overflow-y: auto; + } + .chat-main { + flex: 1 !important; + width: 100% !important; + } + /* Composer 全宽 */ + .chat-composer { + padding: 8px 12px !important; + } + /* 消息气泡减少 padding */ + .markdown-body { + padding: 8px 10px !important; + } + /* 隐藏次要工具栏(等) */ + .chat-toolbar-secondary { + display: none !important; + } +} + +/* ===== 平板(768-1024) ===== */ +@media (min-width: 768px) and (max-width: 1024px) { + .chat-sidebar { + width: 240px !important; + } +} + +/* ===== 暗态增强(虽然 :root + .dark 已支持,但部分 inline color 需降级) ===== */ +.dark .markdown-body { + background-color: var(--color-surface-elevated); + color: var(--color-text); + border-color: var(--color-border); +} +.dark .md-code-block { + background-color: rgba(255, 255, 255, 0.05); + border-color: var(--color-border); +} + +/* ===== 富文本增强:代码块复制按钮 ===== */ +.md-code-block-wrapper { + position: relative; +} +.md-code-copy-btn { + position: absolute; + top: 6px; + right: 6px; + padding: 2px 10px; + font-size: 11px; + border-radius: 4px; + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text-secondary); + cursor: pointer; + opacity: 0; + transition: opacity 200ms; +} +.md-code-block-wrapper:hover .md-code-copy-btn { + opacity: 1; +} +.md-code-copy-btn:hover { + background: var(--color-primary); + color: #fff; +} + +/* ===== 移动端表格响应式 ===== */ +.markdown-body table { + display: block; + overflow-x: auto; + white-space: nowrap; + -webkit-overflow-scrolling: touch; +} + +/* ===== prefers-reduced-motion 降级 ===== */ +@media (prefers-reduced-motion: reduce) { + .md-code-copy-btn, + .chat-sidebar, + .chat-composer { + transition: none !important; + } +} \ No newline at end of file