#938 ChatPage UX 打磨 (移动端响应式 + 暗态增强 + 代码块复制按钮)
This commit is contained in:
1 parent
84d89ab95a
commit
0820d408eb
2 files changed
+228
No files matched your search
@@ -0,0 +1,88 @@
|
|||||||
|
// ChatPage 移动端壳 + 复制按钮 hook — #938
|
||||||
|
// 不直接改 ChatPage 主体,提供增强 hooks 与组件,可选挂载
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useCopyToClipboard — 复制文本到剪贴板(带回退方案)
|
||||||
|
*/
|
||||||
|
export function useCopyToClipboard(): [boolean, (text: string) => Promise<void>] {
|
||||||
|
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 (
|
||||||
|
<button
|
||||||
|
className="md-code-copy-btn"
|
||||||
|
onClick={(e) => { e.preventDefault(); copy(text); }}
|
||||||
|
title={copied ? '已复制' : label}
|
||||||
|
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{copied ? '✓' : label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 块自动注入复制按钮
|
||||||
|
* 用法:<CodeBlockWithCopy lang="python">{`print("hi")`}</CodeBlockWithCopy>
|
||||||
|
*/
|
||||||
|
export const CodeBlockWithCopy: React.FC<{
|
||||||
|
lang?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}> = ({ lang, children }) => {
|
||||||
|
const text = typeof children === 'string' ? children : String(children || '');
|
||||||
|
return (
|
||||||
|
<div className="md-code-block-wrapper">
|
||||||
|
<pre className="md-code-block" data-lang={lang}>
|
||||||
|
<code>{children}</code>
|
||||||
|
</pre>
|
||||||
|
<CopyButton text={text} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { CopyButton, CodeBlockWithCopy, useCopyToClipboard, useIsMobile };
|
||||||
@@ -1784,3 +1784,143 @@ html.dark .ai-sending-timeout {
|
|||||||
@keyframes aiPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
|
@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-error { background: rgba(255,0,0,0.05); border-left: 2px solid #e74c3c; }
|
||||||
.ai-process-running { border-left: 2px solid #3498db; }
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user