docs(claude): 添加 React 事件委托、筛选逻辑、Grid 布局及 UI 高度统一规则

This commit is contained in:
liquidliang committed 2026-01-20 12:00:41 +08:00
1 parent e4cdca4515
commit dcabd75e69
16 files changed
+1300 -721

No files matched your search

+109
View File
@@ -2975,6 +2975,115 @@ export const setStrokeStyle = (board: PlaitBoard, strokeStyle: StrokeStyle) => {
**原因**: `PropertyTransforms` 的默认行为可能不会处理所有类型的元素(如自定义的 Freehand 元素)。通过提供 `callback` 函数,可以确保对所有选中的元素执行属性设置操作。颜色设置(`setStrokeColor`、`setFillColor`)也使用了相同的模式。
### 错误 3: 第三方窗口/弹窗组件破坏 React 事件委托
**场景**: 使用 `WinBox.js` 或其他直接操作 DOM 的第三方窗口库包装 React 组件时
❌ **错误示例**:
```typescript
// 错误:使用 mount 选项将 React 渲染的 DOM 移动到外部,会破坏 React 的事件冒泡链
new WinBox({
mount: containerRef.current, // 导致 onClick/onDoubleClick 无响应
// ...
});
```
✅ **正确示例**:
```typescript
// 正确:使用 React.createPortal 将内容渲染到第三方组件提供的 DOM 容器中
const WinBoxWindow = ({ children }) => {
const [contentRef, setContentRef] = useState<HTMLElement | null>(null);
useEffect(() => {
const winbox = new WinBox({
oncreate: () => {
setContentRef(winbox.body); // 获取 WinBox 提供的容器
}
});
}, []);
return contentRef ? createPortal(children, contentRef) : null;
};
```
**原因**: React 使用事件委托机制在 `root` 节点监听事件。如果第三方库通过 `appendChild` 等原生 API 将 DOM 节点移出 React 的 root 树,事件将无法正常冒泡到 React 的监听器。`createPortal` 允许在物理上移动 DOM 的同时,在逻辑上保持 React 的组件树和事件流完整。
### 错误 4: 筛选逻辑中“全部”选项处理不当
**场景**: 实现带有“全部(ALL)”选项的多重过滤逻辑时
❌ **错误示例**:
```typescript
// 错误:未处理 undefined 情况,导致多条件组合时结果意外为空
const matchesType = filters.activeType === 'ALL' || asset.type === filters.activeType;
// 如果 activeType 是 undefined (初始状态),(undefined === 'ALL') 为 false,逻辑失效
```
✅ **正确示例**:
```typescript
// 正确:显式处理 undefined 和 'ALL',确保逻辑鲁棒
const matchesType =
!filters.activeType ||
filters.activeType === 'ALL' ||
asset.type === filters.activeType;
```
**原因**: 初始状态或重置状态下,筛选变量可能是 `undefined` 或 `null`。在进行比较前必须先进行存在性检查,否则会导致筛选结果不符合预期(通常表现为只有单独筛选有效,组合筛选失效)。
### 错误 5: 动态缩放网格布局出现间隙或重叠
**场景**: 实现支持用户调整元素显示尺寸(放大/缩小)的网格列表时
❌ **错误示例**:
```scss
// 错误:使用 Flex 布局配合动态计算的百分比宽度,容易产生像素计算偏差
.grid-row {
display: flex;
.item {
width: 18.523%; // 计算出的宽度,容易在右侧留下缝隙
}
}
```
✅ **正确示例**:
```scss
// 正确:使用 CSS Grid 布局配合 1fr,确保完美平铺和对齐
.grid-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); // 或动态设置列数
gap: 16px;
.item {
width: 100%;
height: 100%;
}
}
```
**原因**: Flex 布局在处理非整数像素的列宽时,浏览器舍入误差会导致右侧出现白边或元素重叠。CSS Grid 的 `1fr` 单元由浏览器底层引擎处理自动分配,能确保每一列都精准对齐容器边界,尤其适合需要频繁变动尺寸的素材预览场景。
### 错误 6: UI 元素高度不统一导致视觉错位
**场景**: 搜索框、按钮、已选计数条等多个组件并排排列时
❌ **错误示例**:
```scss
.search-input { height: 36px; }
.action-button { height: 32px; }
// 导致并排排列时基准线不齐,视觉凌乱
```
✅ **正确示例**:
```scss
// 正确:统一锁定核心高度(如 32px),并在组件库样式上使用 !important 覆盖
.t-input { height: 32px !important; }
.t-button { height: 32px !important; }
.counter-tag { height: 32px; display: flex; align-items: center; }
```
**原因**: “素雅”和“专业”感来自于严格的视觉对齐。在紧凑的工具栏布局中,即便只有 2-4px 的高度差也会被用户感知。应选定一个标准高度并强制执行,消除视觉噪音。
---
### 性能指南
- 使用 `React.lazy` 对大型组件进行代码分割
- 对图片实现懒加载和预加载
+7 -9
View File
@@ -682,10 +682,11 @@ function renderRelatedRequests(log) {
const status = r.status || '...';
const statusClass = r.status >= 200 && r.status < 400 ? 'success' : (r.status >= 400 ? 'error' : '');
const duration = r.duration ? `${r.duration}ms` : '-';
const displayUrl = r.url?.substring(0, 60) + (r.url?.length > 60 ? '...' : '');
// Show full URL, let CSS handle wrapping
const displayUrl = r.url || '-';
return `
<div class="related-request" data-id="${r.id}" style="padding: 4px 8px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; background: var(--bg-tertiary);">
<div class="related-request" data-id="${r.id}" style="padding: 4px 8px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; background: var(--bg-tertiary); word-break: break-word;">
<span style="color: var(--text-muted); font-size: 11px;">${time}</span>
<span class="log-status ${statusClass}" style="font-size: 11px; margin-left: 8px;">${status}</span>
<span style="margin-left: 8px; font-size: 12px;">${displayUrl}</span>
@@ -1644,10 +1645,8 @@ function createLLMApiEntry(log, isExpanded, onToggle) {
const durationText = log.duration ? `${(log.duration / 1000).toFixed(1)}s` : '-';
const durationClass = durationMs >= 3000 ? 'very-slow' : (durationMs >= 1000 ? 'slow' : '');
// Truncate prompt for header display
const promptPreview = log.prompt
? (log.prompt.length > 60 ? log.prompt.substring(0, 60) + '...' : log.prompt)
: '-';
// Show full prompt in header (will wrap if needed)
const promptPreview = log.prompt || '-';
entry.innerHTML = `
<div class="log-header">
@@ -2106,11 +2105,10 @@ function createCrashEntry(log, isExpanded, onToggle) {
}
}
// Error preview
// Error preview - show full message (will wrap if needed)
let errorPreview = '';
if (log.error) {
const shortError = (log.error.message || '').substring(0, 50);
errorPreview = `<span class="log-url" style="color: var(--error-color);">${escapeHtml(shortError)}${log.error.message?.length > 50 ? '...' : ''}</span>`;
errorPreview = `<span class="log-url" style="color: var(--error-color);">${escapeHtml(log.error.message || '')}</span>`;
}
// 完整内存显示
+68 -26
View File
@@ -21,6 +21,7 @@ function formatStack(stack) {
/**
* Create a console log entry DOM element
* Uses the same styles as Fetch logs for consistency
* @param {object} log
* @param {boolean} isExpanded - Initial expanded state for stack
* @param {Function} onToggle - Callback when expand state changes (id, expanded)
@@ -28,46 +29,87 @@ function formatStack(stack) {
*/
export function createConsoleEntry(log, isExpanded = false, onToggle = null) {
const entry = document.createElement('div');
entry.className = `console-entry ${log.logLevel || 'log'}`;
const level = log.logLevel || 'log';
entry.className = `log-entry console-entry ${level}` + (isExpanded ? ' expanded' : '');
entry.dataset.id = log.id;
const hasStack = log.logStack && log.logStack.trim();
const stackToggle = hasStack
? `<span class="stack-toggle" title="展开/收起堆栈"><span class="arrow">▶</span> 堆栈</span>`
: '';
// Map log level to status class
const levelStatusClass = {
'error': 'error',
'warn': 'redirect',
'info': 'success',
'log': 'pending',
'debug': 'pending',
}[level] || 'pending';
// Show full message in header (will wrap if needed)
const messagePreview = log.logMessage || '-';
entry.innerHTML = `
<div class="console-header">
<div class="log-header">
${hasStack ? `<span class="log-toggle" title="展开/收起详情"><span class="arrow">▶</span></span>` : '<span style="width: 16px; display: inline-block;"></span>'}
<span class="log-time">${formatTime(log.timestamp)}</span>
<span class="console-level ${log.logLevel || 'log'}">${(log.logLevel || 'log').toUpperCase()}</span>
<span class="console-message">${escapeHtml(log.logMessage || '')}</span>
<span class="log-status ${levelStatusClass}">${level.toUpperCase()}</span>
<span class="log-url" title="${escapeHtml(log.logMessage || '')}">${escapeHtml(messagePreview)}</span>
</div>
${log.logSource ? `<div class="console-source">${escapeHtml(log.logSource)}</div>` : ''}
${log.url ? `<div class="console-source">页面: ${escapeHtml(log.url)}</div>` : ''}
${hasStack ? `
<div class="console-stack-container${isExpanded ? ' expanded' : ''}">
${stackToggle}
<pre class="console-stack">${formatStack(log.logStack)}</pre>
${hasStack || log.logSource || log.url ? `
<div class="log-details">
${log.logMessage ? `
<div class="detail-section">
<h4>完整消息</h4>
<pre>${escapeHtml(log.logMessage)}</pre>
</div>
` : ''}
${log.logSource ? `
<div class="detail-section">
<h4>来源</h4>
<pre>${escapeHtml(log.logSource)}</pre>
</div>
` : ''}
${log.url ? `
<div class="detail-section">
<h4>页面</h4>
<pre>${escapeHtml(log.url)}</pre>
</div>
` : ''}
${hasStack ? `
<div class="detail-section">
<h4>堆栈</h4>
<pre style="color: var(--error-color);">${formatStack(log.logStack)}</pre>
</div>
` : ''}
</div>
` : ''}
`;
// Add toggle functionality for stack
if (hasStack) {
const stackContainer = entry.querySelector('.console-stack-container');
const toggle = entry.querySelector('.stack-toggle');
if (toggle && stackContainer) {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const isNowExpanded = stackContainer.classList.toggle('expanded');
if (onToggle) {
onToggle(log.id, isNowExpanded);
}
});
// Toggle function - same as Fetch logs
const toggleExpand = () => {
const isNowExpanded = entry.classList.toggle('expanded');
if (onToggle) {
onToggle(log.id, isNowExpanded);
}
};
// Toggle expand/collapse on button click
const toggleBtn = entry.querySelector('.log-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleExpand();
});
}
// Toggle on header click (except toggle button)
const header = entry.querySelector('.log-header');
header.addEventListener('click', (e) => {
if (e.target.closest('.log-toggle')) return;
if (hasStack || log.logSource || log.url) {
toggleExpand();
}
});
return entry;
}
@@ -26,7 +26,7 @@ function formatData(data, maxLength = 500) {
}
/**
* Get a short preview of the message data
* Get a preview of the message data (will wrap if needed via CSS)
* @param {any} data
* @returns {string}
*/
@@ -35,8 +35,9 @@ function getDataPreview(data) {
try {
const str = JSON.stringify(data);
if (str.length > 60) {
return escapeHtml(str.slice(0, 60)) + '...';
// Show more content, let CSS handle overflow
if (str.length > 200) {
return escapeHtml(str.slice(0, 200)) + '...';
}
return escapeHtml(str);
} catch {
+58 -102
View File
@@ -625,8 +625,9 @@ button .icon-svg {
.log-header {
display: flex;
align-items: center;
align-items: flex-start;
gap: 6px;
flex-wrap: wrap;
}
.log-time {
@@ -642,8 +643,9 @@ button .icon-svg {
border-radius: 3px;
font-size: 10px;
font-weight: 600;
background: var(--primary-color);
color: #fff;
background: var(--primary-light);
color: var(--primary-color);
border: 1px solid var(--primary-color);
letter-spacing: 0.02em;
}
@@ -652,26 +654,31 @@ button .icon-svg {
border-radius: 3px;
font-size: 10px;
font-weight: 600;
border: 1px solid;
}
.log-status.success {
background: var(--success-light);
color: var(--success-color);
border-color: var(--success-color);
}
.log-status.redirect {
background: var(--warning-light);
color: var(--warning-color);
border-color: var(--warning-color);
}
.log-status.error {
background: var(--error-light);
color: var(--error-color);
border-color: var(--error-color);
}
.log-status.network-error {
background: var(--error-light);
color: var(--error-color);
border-color: var(--error-color);
font-size: 9px;
padding: 2px 5px;
max-width: 120px;
@@ -682,36 +689,40 @@ button .icon-svg {
}
.log-status.pending {
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
background: var(--bg-tertiary);
color: var(--text-muted);
border-color: var(--border-color);
font-style: italic;
}
/* 统一的类型标签样式 */
.log-type-badge {
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
background: var(--bg-primary);
font-weight: 500;
background: var(--bg-tertiary);
color: var(--text-secondary);
border: 1px solid var(--border-color);
white-space: nowrap;
}
.log-type-badge.xhr {
background: var(--accent-color);
color: #fff;
border: none;
background: var(--primary-light);
color: var(--primary-color);
border-color: var(--primary-color);
}
.log-type-badge.sw-internal {
background: #8250df;
color: #fff;
border: none;
background: rgba(90, 79, 207, 0.1);
color: var(--info-color);
border-color: var(--info-color);
}
.log-type-badge.passthrough {
background: var(--text-muted);
color: #fff;
border: none;
background: var(--bg-tertiary);
color: var(--text-muted);
border-color: var(--border-color);
}
.log-purpose {
@@ -722,13 +733,13 @@ button .icon-svg {
.log-url {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
color: var(--text-secondary);
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 11px;
transition: color 0.1s ease;
word-break: break-word;
line-height: 1.4;
}
.log-entry:hover .log-url {
@@ -751,15 +762,17 @@ button .icon-svg {
font-weight: 600;
background: var(--success-light);
color: var(--success-color);
border: 1px solid var(--success-color);
}
.log-streaming {
padding: 1px 4px;
border-radius: 3px;
font-size: 9px;
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
font-weight: 600;
background: rgba(90, 79, 207, 0.1);
color: var(--info-color);
border: 1px solid var(--info-color);
}
/* Log Toggle */
@@ -966,22 +979,15 @@ button .icon-svg {
background: repeating-conic-gradient(#f0f0f0 0% 25%, #fff 0% 50%) 50% / 16px 16px;
}
/* Console Entry */
.console-entry {
padding: 8px 16px;
border-bottom: 1px solid var(--border-color);
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 12px;
}
/* Console Entry - 继承 log-entry 样式,添加左侧边框指示日志级别 */
.console-entry.warn {
background: rgba(210, 153, 34, 0.1);
border-left: 3px solid var(--warning-color);
background: var(--warning-light);
}
.console-entry.error {
background: rgba(248, 81, 73, 0.1);
border-left: 3px solid var(--error-color);
background: var(--error-light);
}
.console-entry.info {
@@ -993,54 +999,8 @@ button .icon-svg {
}
.console-entry.debug {
border-left: 3px solid #8250df;
background: rgba(130, 80, 223, 0.05);
}
.console-header {
display: flex;
align-items: flex-start;
gap: 10px;
}
.console-level {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
flex-shrink: 0;
}
.console-level.warn {
background: var(--warning-color);
color: #fff;
}
.console-level.error {
background: var(--error-color);
color: #fff;
}
.console-level.info {
background: var(--info-color);
color: #fff;
}
.console-level.log {
background: var(--text-muted);
color: #fff;
}
.console-level.debug {
background: #8250df;
color: #fff;
}
.console-message {
flex: 1;
white-space: pre-wrap;
word-break: break-all;
border-left: 3px solid var(--info-color);
background: rgba(90, 79, 207, 0.05);
}
.console-source {
@@ -1262,9 +1222,10 @@ button .icon-svg {
.pm-header {
display: flex;
align-items: center;
align-items: flex-start;
gap: 6px;
cursor: pointer;
flex-wrap: wrap;
}
/* 复用 .log-toggle 样式 */
@@ -1317,16 +1278,19 @@ button .icon-svg {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.02em;
border: 1px solid;
}
.pm-direction.send {
background: var(--primary-color);
color: #fff;
background: var(--primary-light);
color: var(--primary-color);
border-color: var(--primary-color);
}
.pm-direction.receive {
background: var(--success-color);
color: #fff;
background: var(--success-light);
color: var(--success-color);
border-color: var(--success-color);
}
.pm-direction-icon {
@@ -1346,14 +1310,12 @@ button .icon-svg {
.pm-preview {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-secondary);
min-width: 0;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 11px;
color: var(--text-muted);
font-size: 11px;
word-break: break-word;
line-height: 1.4;
}
.pm-details {
@@ -1589,32 +1551,22 @@ button .icon-svg {
/* 继承 .log-entry 的基础样式,不需要额外边框 */
}
.memory-entry .log-type-badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
background: var(--bg-tertiary);
color: var(--text-secondary);
border: 1px solid var(--border-color);
white-space: nowrap;
}
.memory-entry .log-type-badge.error {
background: var(--error-light);
color: var(--error-color);
border-color: transparent;
border-color: var(--error-color);
}
.memory-entry .log-type-badge.warning {
background: var(--warning-light);
color: var(--warning-color);
border-color: transparent;
border-color: var(--warning-color);
}
.memory-entry .log-type-badge.normal {
background: var(--bg-tertiary);
color: var(--text-secondary);
border-color: var(--border-color);
}
/* 内存使用徽章 */
@@ -1624,21 +1576,25 @@ button .icon-svg {
font-size: 10px;
font-weight: 600;
font-family: 'SF Mono', Monaco, Consolas, monospace;
border: 1px solid;
}
.log-memory-badge.normal {
background: var(--success-light);
color: var(--success-color);
border-color: var(--success-color);
}
.log-memory-badge.warning {
background: var(--warning-light);
color: var(--warning-color);
border-color: var(--warning-color);
}
.log-memory-badge.critical {
background: var(--error-light);
color: var(--error-color);
border-color: var(--error-color);
}
/* 内存日志行内信息 */
+3 -6
View File
@@ -80,15 +80,12 @@ export function formatJsonOrText(text) {
* @param {number} maxLength
* @returns {string}
*/
export function extractDisplayUrl(url, maxLength = 80) {
export function extractDisplayUrl(url) {
if (!url) return '-';
try {
const urlObj = new URL(url);
let displayUrl = urlObj.pathname + urlObj.search;
if (displayUrl.length > maxLength) {
displayUrl = displayUrl.substring(0, maxLength) + '...';
}
return displayUrl;
// Return full path without truncation (CSS will handle overflow)
return urlObj.pathname + urlObj.search;
} catch {
return url;
}
@@ -1,47 +1,72 @@
/**
* Asset Item Styles
* 统一素材项样式 - 支持网格、紧凑、列表三种视图模式
* 使用 CSS 过渡实现平滑切换
* 简洁素雅风格
*/
// ========================================
// 品牌色变量(仅用于强调)
// ========================================
$brand-orange: #F39C12;
$brand-orange-light: #FBBF24;
$brand-orange-dark: #E67E22;
.asset-item {
position: relative;
cursor: pointer;
background: white;
background: #ffffff;
border: 2px solid transparent;
overflow: hidden;
// 使用 transform 和 opacity 实现 GPU 加速的过渡
// GPU 加速的过渡动画
transition:
transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
border-color 0.2s ease,
box-shadow 0.2s ease,
box-shadow 0.25s ease,
background-color 0.15s ease;
// 优化渲染性能
will-change: border-color, box-shadow;
will-change: transform, box-shadow;
contain: layout style;
&:hover {
border-color: #c7d2fe;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transform: translateY(-4px) scale(1.02);
border-color: rgba($brand-orange, 0.3);
box-shadow:
0 12px 28px -8px rgba($brand-orange, 0.25),
0 4px 12px -4px rgba(0, 0, 0, 0.1);
}
&--selected {
border-color: #6366f1;
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.3), 0 2px 4px -1px rgba(99, 102, 241, 0.2);
border-color: $brand-orange;
box-shadow:
0 8px 24px -6px rgba($brand-orange, 0.35),
0 0 0 3px rgba($brand-orange, 0.15);
&::before {
content: '';
position: absolute;
inset: -2px;
border-radius: inherit;
background: rgba($brand-orange, 0.08);
z-index: -1;
}
}
&--selection-mode {
cursor: default;
&:hover {
transform: translateY(-2px) scale(1.01);
}
}
// ========== 缩略图 ==========
&__thumbnail {
position: relative;
background: #f3f4f6;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
// 优化图片渲染
content-visibility: auto;
}
@@ -49,61 +74,72 @@
&__video {
width: 100%;
height: 100%;
object-fit: contain; // 显示完整图片,不裁剪
// 图片加载优化
object-fit: contain;
image-rendering: -webkit-optimize-contrast;
transition: transform 0.3s ease;
}
&:hover &__image,
&:hover &__video {
transform: scale(1.05);
}
// ========== 徽章 ==========
&__badges {
position: absolute;
top: 8px;
left: 8px;
right: 8px;
top: 10px;
left: 10px;
right: 10px;
display: flex;
gap: 4px;
gap: 6px;
z-index: 2;
opacity: 1;
transition: opacity 0.15s ease;
transition: opacity 0.2s ease, transform 0.2s ease;
}
&:hover &__badges {
transform: translateY(-2px);
}
&__type-badge {
padding: 4px 8px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
padding: 5px 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid rgba(229, 231, 235, 0.5);
box-shadow:
0 2px 8px 0 rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(255, 255, 255, 0.5);
svg {
color: #6366f1;
width: 12px;
height: 12px;
color: $brand-orange;
width: 13px;
height: 13px;
}
}
&__ai-badge {
padding: 4px 8px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
color: #6366f1;
padding: 5px 10px;
border-radius: 8px;
background: $brand-orange;
color: #ffffff;
font-size: 10px;
font-weight: 500;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.02em;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid rgba(229, 231, 235, 0.5);
letter-spacing: 0.05em;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 2px;
gap: 3px;
line-height: 12px;
&--list {
flex-shrink: 0;
padding: 4px 8px;
font-size: 9px;
}
}
@@ -111,12 +147,20 @@
&__checkbox {
&--overlay {
position: absolute;
top: 8px;
right: 8px;
top: 10px;
right: 10px;
z-index: 3;
border-radius: 6px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 2px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
&--left {
@@ -128,26 +172,32 @@
&__overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 50%,
rgba(0, 0, 0, 0.7) 100%
);
opacity: 0;
transition: opacity 0.2s ease;
transition: opacity 0.25s ease;
pointer-events: none;
}
&__name-overlay {
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
bottom: 14px;
left: 14px;
right: 14px;
color: white;
font-size: 12px;
font-weight: 500;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0;
transition: opacity 0.2s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
transform: translateY(8px);
transition: opacity 0.25s ease, transform 0.25s ease;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
z-index: 1;
}
@@ -162,8 +212,8 @@
&__name {
font-size: 14px;
font-weight: 500;
color: #111827;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -174,7 +224,7 @@
align-items: center;
gap: 12px;
font-size: 12px;
color: #6b7280;
color: #64748b;
}
&__type {
@@ -183,13 +233,13 @@
gap: 4px;
svg {
color: #6366f1;
color: $brand-orange;
}
}
&__size,
&__date {
color: #9ca3af;
color: #94a3b8;
}
// ========================================
@@ -198,11 +248,15 @@
&--grid {
width: 100%;
height: 100%;
border-radius: 12px;
border-radius: 16px;
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.05),
0 0 0 1px rgba(0, 0, 0, 0.03);
.asset-item__thumbnail {
width: 100%;
height: 100%;
border-radius: 14px;
}
&:hover,
@@ -211,6 +265,10 @@
.asset-item__name-overlay {
opacity: 1;
}
.asset-item__name-overlay {
transform: translateY(0);
}
}
&.asset-item--selection-mode:hover {
@@ -226,11 +284,13 @@
&--compact {
width: 100%;
height: 100%;
border-radius: 4px;
border-radius: 8px;
border-width: 1px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
&:hover {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
transform: translateY(-2px) scale(1.01);
box-shadow: 0 6px 16px -4px rgba($brand-orange, 0.2);
}
&.asset-item--selected {
@@ -240,12 +300,36 @@
.asset-item__thumbnail {
width: 100%;
height: 100%;
border-radius: 6px;
}
.asset-item__checkbox--overlay {
top: 4px;
right: 4px;
border-radius: 4px;
top: 6px;
right: 6px;
border-radius: 6px;
padding: 1px;
}
.asset-item__badges {
top: 6px;
left: 6px;
gap: 4px;
}
.asset-item__type-badge {
padding: 3px 6px;
border-radius: 5px;
svg {
width: 10px;
height: 10px;
}
}
.asset-item__ai-badge {
padding: 3px 6px;
border-radius: 5px;
font-size: 8px;
}
}
@@ -255,28 +339,31 @@
&--list {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
gap: 14px;
padding: 12px 14px;
border-radius: 12px;
border-width: 1px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
&:hover {
background: #f9fafb;
border-color: #e5e7eb;
transform: translateX(4px);
background: linear-gradient(90deg, rgba($brand-orange, 0.04) 0%, transparent 100%);
border-color: rgba($brand-orange, 0.2);
box-shadow: none;
}
&.asset-item--selected {
background: #eef2ff;
border-color: #c7d2fe;
background: rgba($brand-orange, 0.06);
border-color: rgba($brand-orange, 0.3);
box-shadow: none;
}
.asset-item__thumbnail {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 6px;
width: 52px;
height: 52px;
border-radius: 10px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.08);
}
}
@@ -284,50 +371,70 @@
// 移动端优化
// ========================================
@media (max-width: 767px) {
&:hover {
transform: none;
}
&--grid {
border-radius: 10px;
border-radius: 12px;
.asset-item__thumbnail {
border-radius: 10px;
}
.asset-item__badges {
top: 6px;
left: 6px;
right: 6px;
gap: 3px;
top: 8px;
left: 8px;
right: 8px;
gap: 4px;
}
.asset-item__type-badge {
padding: 3px 6px;
padding: 4px 8px;
border-radius: 6px;
svg {
width: 10px;
height: 10px;
width: 11px;
height: 11px;
}
}
.asset-item__ai-badge {
padding: 3px 6px;
padding: 4px 8px;
border-radius: 6px;
font-size: 9px;
line-height: 10px;
line-height: 11px;
}
.asset-item__name-overlay {
bottom: 8px;
left: 8px;
right: 8px;
font-size: 11px;
bottom: 10px;
left: 10px;
right: 10px;
font-size: 12px;
}
}
&--compact {
border-radius: 2px;
border-radius: 6px;
.asset-item__thumbnail {
border-radius: 4px;
}
}
&--list {
padding: 8px 10px;
gap: 10px;
padding: 10px 12px;
gap: 12px;
border-radius: 10px;
&:hover {
transform: none;
}
.asset-item__thumbnail {
width: 40px;
height: 40px;
width: 44px;
height: 44px;
border-radius: 8px;
}
.asset-item__name {
@@ -344,8 +451,8 @@
}
.asset-item__ai-badge--list {
padding: 2px 6px;
font-size: 9px;
padding: 3px 6px;
font-size: 8px;
}
}
}
@@ -1,14 +1,24 @@
/**
* Media Library Grid Styles
* 素材库网格视图样式 - Tailwind-inspired design
* 素材库网格视图样式 - 简洁素雅风格
*/
// ========================================
// 品牌色变量(仅用于强调)
// ========================================
$brand-orange: #F39C12;
$brand-orange-light: #FBBF24;
$brand-orange-dark: #E67E22;
.media-library-grid {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
min-height: 0; // 重要:允许 flex 子元素收缩
overflow: hidden;
position: relative;
background: #ffffff;
&--dragging {
.media-library-grid__container {
@@ -16,50 +26,57 @@
}
}
// ========== 拖放遮罩 ==========
&__drop-overlay {
position: absolute;
inset: 16px;
background: rgba(99, 102, 241, 0.05);
border: 4px dashed #6366f1;
border-radius: 16px;
background: rgba($brand-orange, 0.05);
border: 4px dashed $brand-orange;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: none;
backdrop-filter: blur(4px);
}
&__drop-message {
padding: 40px 64px;
padding: 48px 72px;
background: white;
border-radius: 16px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
border: 1px solid #e0e7ff;
border-radius: 20px;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba($brand-orange, 0.1);
border: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
&-icon {
width: 64px;
height: 64px;
background: #eef2ff;
width: 72px;
height: 72px;
background: rgba($brand-orange, 0.06);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #6366f1;
color: $brand-orange;
box-shadow: 0 8px 24px -8px rgba($brand-orange, 0.3);
}
&-title {
font-size: 20px;
font-size: 22px;
font-weight: 700;
color: #111827;
color: #1e293b;
margin: 0;
}
&-description {
font-size: 14px;
color: #6b7280;
color: #64748b;
margin: 0;
}
}
@@ -68,13 +85,14 @@
align-items: center;
justify-content: center;
height: 100%;
background: #f8fafc;
}
// 骨架屏遮盖层 - 视图切换时显示
// ========== 骨架屏遮盖层 ==========
&__skeleton-overlay {
position: absolute;
inset: 0;
background: #fafbfc;
background: #f8fafc;
z-index: 10;
display: flex;
flex-wrap: wrap;
@@ -93,48 +111,122 @@
}
}
// ========== 头部工具栏 ==========
&__header {
padding: 12px 20px;
border-bottom: 1px solid #e5e7eb;
background: white;
padding: 14px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: 16px;
}
&__header-actions {
display: flex;
align-items: center;
gap: 12px;
gap: 14px;
}
&__search {
flex: 1;
max-width: 350px;
max-width: 380px;
.t-input {
border-radius: 10px;
border-color: rgba(0, 0, 0, 0.08);
background: #f8fafc;
transition: all 0.2s ease;
&:hover {
border-color: rgba($brand-orange, 0.3);
background: #ffffff;
}
&:focus,
&.t-is-focused {
border-color: $brand-orange;
background: #ffffff;
box-shadow: 0 0 0 3px rgba($brand-orange, 0.12);
}
}
}
&__count {
font-size: 13px;
font-weight: 500;
color: #6b7280;
color: #64748b;
white-space: nowrap;
}
&__selection-count {
font-size: 13px;
font-weight: 600;
color: #6366f1;
font-weight: 700;
color: $brand-orange;
white-space: nowrap;
padding: 4px 10px;
background: rgba($brand-orange, 0.1);
border-radius: 6px;
}
// ========== 按钮样式覆盖 ==========
.t-button--theme-primary {
background: $brand-orange !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600;
box-shadow: none;
transition: all 0.2s ease;
&:hover {
background: $brand-orange-dark !important;
}
}
.t-button--variant-outline {
border-radius: 10px !important;
font-weight: 500;
border-color: rgba(0, 0, 0, 0.1);
color: #475569;
transition: all 0.2s ease;
&:hover {
border-color: rgba($brand-orange, 0.3);
color: $brand-orange;
background: rgba($brand-orange, 0.04);
}
}
.t-button--theme-danger {
border-radius: 8px !important;
&.t-button--variant-base {
background: #dc3545 !important;
border: none !important;
box-shadow: none;
&:hover {
background: #c82333 !important;
}
}
}
// ========== 内容容器 ==========
&__container {
flex: 1;
overflow: hidden;
background: #fafbfc;
min-height: 0; // 重要:允许 flex 子元素收缩
overflow: auto; // 改为 auto 允许滚动
background: #f8fafc;
position: relative;
display: flex;
flex-direction: column;
// 确保虚拟网格填满容器
.virtual-asset-grid {
flex: 1;
min-height: 0;
}
// 过渡状态 - 内容淡出
&--transitioning {
.virtual-asset-grid {
opacity: 0.3;
@@ -143,29 +235,38 @@
}
}
// ========== 底部状态栏 ==========
&__footer {
height: 36px;
border-top: 1px solid #e5e7eb;
background: white;
height: 40px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
font-size: 12px;
font-weight: 500;
color: #6b7280;
color: #64748b;
&-hint {
color: #6366f1;
color: $brand-orange;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
&::before {
content: '⚡';
font-size: 11px;
}
}
}
// 移动端优化
// ========== 移动端优化 ==========
@media (max-width: 768px) {
&__header {
padding: 10px 12px;
gap: 8px;
padding: 12px 14px;
gap: 10px;
flex-wrap: wrap;
}
@@ -188,17 +289,18 @@
}
&__footer {
padding: 0 12px;
padding: 0 14px;
font-size: 11px;
height: 32px;
height: 36px;
}
&__drop-message {
padding: 32px 48px;
padding: 36px 48px;
border-radius: 16px;
&-icon {
width: 56px;
height: 56px;
width: 60px;
height: 60px;
}
&-title {
@@ -213,15 +315,16 @@
@media (max-width: 480px) {
&__header {
padding: 8px 10px;
padding: 10px 12px;
}
&__drop-message {
padding: 24px 32px;
padding: 28px 36px;
border-radius: 14px;
&-icon {
width: 48px;
height: 48px;
width: 52px;
height: 52px;
}
&-title {
@@ -235,7 +338,7 @@
}
}
// 空状态样式
// ========== 空状态 ==========
.media-library-empty {
display: flex;
flex-direction: column;
@@ -244,30 +347,34 @@
height: 100%;
padding: 48px 24px;
text-align: center;
background: #f8fafc;
&__icon {
width: 80px;
height: 80px;
background: #f3f4f6;
width: 88px;
height: 88px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #d1d5db;
margin-bottom: 24px;
color: #cbd5e1;
margin-bottom: 28px;
box-shadow:
0 8px 24px -8px rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(0, 0, 0, 0.04);
}
&__title {
font-size: 18px;
font-size: 20px;
font-weight: 700;
color: #111827;
margin-bottom: 8px;
color: #1e293b;
margin-bottom: 10px;
}
&__description {
font-size: 14px;
color: #6b7280;
color: #64748b;
max-width: 400px;
line-height: 1.5;
line-height: 1.6;
}
}
@@ -1,15 +1,22 @@
/**
* Media Library Inspector Styles
* 素材库详情面板样式 - Tailwind-inspired design
* 素材库详情面板样式 - 简洁素雅风格
*/
// ========================================
// 品牌色变量(仅用于强调)
// ========================================
$brand-orange: #F39C12;
$brand-orange-light: #FBBF24;
$brand-orange-dark: #E67E22;
.media-library-inspector {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
gap: 12px;
background: linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%);
padding: 20px;
gap: 16px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
min-width: 280px;
overflow-y: auto;
@@ -23,85 +30,55 @@
}
&::-webkit-scrollbar-thumb {
background: #d1d5db;
background: #e2e8f0;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
background: #cbd5e1;
}
// 空状态
&--empty {
align-items: center;
justify-content: center;
color: #9ca3af;
color: #94a3b8;
font-size: 14px;
text-align: center;
padding: 32px 16px;
padding: 40px 20px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
p {
margin: 0;
font-weight: 500;
}
}
&--empty &-icon {
width: 80px;
height: 80px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
svg {
width: 32px;
height: 32px;
opacity: 0.4;
}
}
&--empty &-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
}
&--empty &-description {
font-size: 14px;
color: #6b7280;
}
&__header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
background: rgba(255, 255, 255, 0.5);
&-title {
font-size: 12px;
font-weight: 700;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
// ========== 预览区域 ==========
&__preview {
width: 100%;
height: 180px;
border-radius: 12px;
height: 200px;
border-radius: 16px;
overflow: hidden;
background: white;
border: 1px solid #e5e7eb;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
padding: 12px;
box-shadow:
0 4px 12px 0 rgba(0, 0, 0, 0.04),
inset 0 1px 0 0 rgba(255, 255, 255, 0.8);
flex-shrink: 0;
position: relative;
&::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.02) 0%, transparent 70%);
pointer-events: none;
}
}
&__image,
@@ -111,9 +88,12 @@
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
border-radius: 10px;
position: relative;
z-index: 1;
}
// ========== 名称区域 ==========
&__name-section {
display: flex;
flex-direction: column;
@@ -122,25 +102,19 @@
min-height: 36px;
}
&__label {
font-size: 11px;
font-weight: 600;
color: #6b7280;
}
&__name-display {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
padding: 6px 0;
}
&__name {
flex: 1;
margin: 0;
font-size: 14px;
font-weight: 600;
color: #111827;
font-size: 16px;
font-weight: 700;
color: #1e293b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -151,22 +125,29 @@
display: flex;
flex-direction: column;
gap: 8px;
.t-input {
border-radius: 10px;
border-color: rgba($brand-orange, 0.3);
&:focus,
&.t-is-focused {
border-color: $brand-orange;
box-shadow: 0 0 0 3px rgba($brand-orange, 0.15);
}
}
}
&__name-actions {
display: flex;
gap: 8px;
}
// ========== 元数据卡片 ==========
&__metadata {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: white;
border-radius: 10px;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04);
gap: 0;
padding: 0;
background: #ffffff;
border-radius: 14px;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.03);
flex-shrink: 0;
overflow: hidden;
}
@@ -175,50 +156,65 @@
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
min-height: 20px;
font-size: 13px;
min-height: 44px;
padding: 0 16px;
gap: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
transition: background-color 0.15s ease;
&:last-child {
border-bottom: none;
}
&:hover {
background: rgba($brand-orange, 0.02);
}
}
&__meta-label {
color: #6b7280;
color: #64748b;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
gap: 6px;
white-space: nowrap;
flex-shrink: 0;
svg {
width: 13px;
height: 13px;
width: 14px;
height: 14px;
color: $brand-orange;
}
}
&__meta-value {
color: #111827;
color: #1e293b;
font-weight: 600;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: 12px;
flex-shrink: 1;
background: rgba($brand-orange, 0.06);
padding: 4px 10px;
border-radius: 6px;
}
// 提示词区域样式
// ========== 提示词区域 ==========
&__prompt-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: white;
border-radius: 10px;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04);
gap: 10px;
padding: 16px;
background: #ffffff;
border-radius: 14px;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.03);
flex: 1;
min-height: 80px;
min-height: 100px;
overflow: hidden;
}
@@ -230,19 +226,34 @@
}
&__prompt-label {
font-size: 12px;
font-weight: 600;
color: #374151;
font-size: 13px;
font-weight: 700;
color: #1e293b;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: '';
width: 3px;
height: 14px;
background: $brand-orange;
border-radius: 2px;
}
}
&__prompt-content {
font-size: 13px;
line-height: 1.7;
color: #4b5563;
line-height: 1.8;
color: #475569;
overflow-y: auto;
flex: 1;
word-break: break-word;
white-space: pre-wrap;
padding: 12px;
background: rgba($brand-orange, 0.03);
border-radius: 10px;
border: 1px solid rgba($brand-orange, 0.08);
// 自定义滚动条
&::-webkit-scrollbar {
@@ -254,51 +265,118 @@
}
&::-webkit-scrollbar-thumb {
background: #d1d5db;
background: #e2e8f0;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
background: #cbd5e1;
}
}
// ========== 操作按钮 ==========
&__actions {
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
margin-top: auto;
padding-top: 12px;
padding-top: 16px;
flex-shrink: 0;
// 按钮样式优化
.t-button {
border-radius: 8px;
// 主要按钮 - 素雅风格
.t-button--theme-primary {
background: $brand-orange !important;
border: none !important;
border-radius: 10px !important;
font-weight: 600;
height: 40px;
box-shadow: none;
transition: all 0.2s ease;
&:hover {
background: $brand-orange-dark !important;
}
&:active {
background: darken($brand-orange-dark, 5%) !important;
}
}
// 次要按钮
.t-button--variant-outline {
border-radius: 12px !important;
font-weight: 500;
height: 40px;
border-color: rgba(0, 0, 0, 0.1);
color: #475569;
transition: all 0.2s ease;
&:hover {
border-color: rgba($brand-orange, 0.3);
color: $brand-orange;
background: rgba($brand-orange, 0.04);
}
}
// 危险按钮
.t-button--theme-danger {
border-radius: 10px !important;
font-weight: 500;
height: 36px;
&.t-button--variant-outline {
border-color: rgba(220, 53, 69, 0.3);
color: #dc3545;
&:hover {
background: rgba(220, 53, 69, 0.08);
border-color: #dc3545;
}
}
}
}
// 移动端优化
// ========== 移动端优化 ==========
@media (max-width: 768px) {
padding: 16px;
gap: 16px;
gap: 14px;
&__preview {
height: auto;
aspect-ratio: 16 / 9;
border-radius: 14px;
}
&__name {
font-size: 14px;
font-size: 15px;
}
&__metadata {
gap: 12px;
padding: 12px;
border-radius: 12px;
}
&__meta-item {
gap: 8px;
min-height: 40px;
padding: 0 14px;
font-size: 12px;
}
&__prompt-section {
padding: 14px;
border-radius: 12px;
}
&__actions {
.t-button--theme-primary {
height: 42px;
border-radius: 10px !important;
}
.t-button--variant-outline,
.t-button--theme-danger {
height: 38px;
border-radius: 10px !important;
}
}
}
}
@@ -1,133 +1,152 @@
/**
* Media Library Modal Styles
* 素材库弹窗样式 - Tailwind-inspired design
* 素材库弹窗样式 - WinBox 窗口 + 简洁素雅主题
*/
// TDesign Dialog 样式覆盖 - 使用全局选择器确保生效
.media-library-modal {
&.t-dialog__ctx {
.t-dialog__body {
padding: 0 !important;
margin: 0 !important;
height: 75vh;
max-height: 75vh;
background: white;
@import '../../styles/z-index.scss';
// 移动端适配
@media (max-width: 768px) {
height: 85vh;
max-height: 85vh;
}
}
// ========================================
// 品牌色变量(仅用于强调元素)
// ========================================
$brand-orange: #F39C12;
$brand-orange-light: #FBBF24;
$brand-orange-dark: #E67E22;
.t-dialog__wrap {
background: rgba(17, 24, 39, 0.6);
backdrop-filter: blur(4px);
}
// ========================================
// WinBox 素材库专用样式 - 素雅风格
// ========================================
.winbox-media-library {
z-index: $z-dialog-ai-image !important;
border-radius: 12px !important;
overflow: hidden;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(0, 0, 0, 0.05) !important;
.t-dialog {
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(229, 231, 235, 0.5);
overflow: hidden;
background: white;
padding: 0 !important;
// 移动端适配
@media (max-width: 768px) {
width: 95vw !important;
max-width: 95vw;
border-radius: 12px;
}
}
.t-dialog__header {
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 12px 16px !important;
margin: 0 !important;
}
// 标题栏 - 白色简洁风格
.wb-header {
background: #ffffff !important;
border-radius: 12px 12px 0 0;
height: 44px;
border-bottom: 1px solid #e5e5e5;
}
&__header {
// 标题文字
.wb-title {
font-family: 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
font-weight: 600;
color: #333333;
padding: 0 8px 0 12px;
}
// 标题栏图标区域
.wb-header-custom {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 12px;
margin-left: 4px;
}
@media (max-width: 768px) {
gap: 8px;
}
// 控制按钮 - 灰黑色风格
.wb-min,
.wb-max,
.wb-full,
.wb-close {
width: 32px;
height: 32px;
border-radius: 6px;
background-size: 14px;
filter: invert(1) brightness(0.3);
transition: all 0.15s ease-out;
&-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 700;
color: #111827;
@media (max-width: 768px) {
font-size: 16px;
gap: 6px;
}
&-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
@media (max-width: 768px) {
width: 28px;
height: 28px;
border-radius: 6px;
svg {
width: 14px;
height: 14px;
}
}
}
&:hover {
background-color: rgba(0, 0, 0, 0.08);
filter: invert(1) brightness(0.2);
}
}
}
// 移动端详情抽屉样式
.media-library-mobile-drawer {
.t-drawer__body {
.wb-close:hover {
background-color: rgba(255, 59, 48, 0.15);
filter: invert(1) brightness(0.2);
}
// 窗口主体
.wb-body {
background: #ffffff;
border-radius: 0 0 12px 12px;
overflow: hidden;
padding: 0;
background: #f9fafb;
display: flex;
flex-direction: column;
}
.t-drawer__header {
border-bottom: 1px solid #e5e7eb;
padding: 16px 20px;
background: white;
// 内容包装器
.winbox-content-wrapper {
padding: 0 !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
background: #f8fafc !important;
display: flex !important;
flex-direction: column !important;
flex: 1 !important;
}
// 聚焦状态
&.focus {
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(0, 0, 0, 0.1) !important;
}
// 最大化状态
&.max {
border-radius: 0 !important;
.wb-header {
border-radius: 0;
}
.wb-body {
border-radius: 0;
}
}
}
// 标题栏内容
.media-library-header-content {
display: flex;
align-items: center;
color: #666666;
svg {
width: 16px;
height: 16px;
}
}
// ========================================
// 布局容器
// ========================================
.media-library-layout {
display: flex;
flex: 1;
height: 100%;
background: white;
min-height: 0; // 重要:允许 flex 子元素收缩
background: #f8fafc;
overflow: hidden;
&__sidebar {
width: 220px;
border-right: 1px solid #e5e7eb;
background: #f9fafb;
border-right: 1px solid rgba(0, 0, 0, 0.06);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
display: flex;
flex-direction: column;
padding: 14px;
overflow-y: auto; // 改回auto,允许滚动
flex-shrink: 0; // 防止被压缩
padding: 16px;
overflow-y: auto;
flex-shrink: 0;
// 自定义滚动条样式
// 自定义滚动条
&::-webkit-scrollbar {
width: 6px;
}
@@ -137,11 +156,11 @@
}
&::-webkit-scrollbar-thumb {
background: #d1d5db;
background: #e2e8f0;
border-radius: 3px;
&:hover {
background: #9ca3af;
background: #cbd5e1;
}
}
}
@@ -151,20 +170,20 @@
display: flex;
flex-direction: column;
overflow: hidden;
background: white;
background: #ffffff;
min-width: 0;
}
&__inspector {
width: 280px;
border-left: 1px solid #e5e7eb;
background: #f9fafb;
width: 300px;
border-left: 1px solid rgba(0, 0, 0, 0.06);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
display: flex;
flex-direction: column;
overflow-y: auto; // 改回auto,允许滚动
flex-shrink: 0; // 防止被压缩
overflow-y: auto;
flex-shrink: 0;
// 自定义滚动条样式
// 自定义滚动条
&::-webkit-scrollbar {
width: 6px;
}
@@ -174,11 +193,11 @@
}
&::-webkit-scrollbar-thumb {
background: #d1d5db;
background: #e2e8f0;
border-radius: 3px;
&:hover {
background: #9ca3af;
background: #cbd5e1;
}
}
}
@@ -187,10 +206,11 @@
@media (max-width: 1024px) {
&__sidebar {
width: 200px;
padding: 12px;
}
&__inspector {
width: 260px;
width: 280px;
}
}
@@ -200,39 +220,84 @@
&__sidebar {
width: 100%;
height: auto;
max-height: 120px; // 减小高度,给图片区域更多空间
max-height: 100px;
border-right: none;
border-bottom: 1px solid #e5e7eb;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding: 10px;
overflow-y: auto; // 允许滚动查看所有筛选项
overflow-y: auto;
}
&__main {
flex: 1;
min-height: 0;
// 确保主区域获得足够空间
}
&__inspector {
display: none; // 移动端隐藏详情面板,节省空间
// 或者改为可折叠的形式
display: none;
width: 100%;
height: auto;
max-height: 40vh;
border-left: none;
border-top: 1px solid #e5e7eb;
border-top: 1px solid rgba(0, 0, 0, 0.06);
min-width: auto;
}
}
@media (max-width: 480px) {
&__sidebar {
max-height: 100px; // 更小屏幕进一步减小
max-height: 90px;
padding: 8px;
}
&__inspector {
max-height: 35vh; // 如果显示的话,也减小高度
}
}
}
// ========================================
// 移动端详情抽屉
// ========================================
.media-library-mobile-drawer {
.t-drawer__body {
padding: 0;
background: #f8fafc;
}
.t-drawer__header {
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding: 16px 20px;
background: white;
font-weight: 600;
color: #1e293b;
}
.t-drawer {
border-radius: 20px 20px 0 0;
}
}
// ========================================
// 响应式 WinBox 调整
// ========================================
@media (max-width: 768px) {
.winbox-media-library {
.wb-header {
height: 44px;
}
.wb-title {
font-size: 14px;
}
.wb-min,
.wb-max,
.wb-full,
.wb-close {
width: 30px;
height: 30px;
background-size: 12px;
}
.media-library-header-content svg {
width: 16px;
height: 16px;
}
}
}
@@ -1,16 +1,16 @@
/**
* Media Library Modal
* 素材库弹窗容器组件
* 素材库弹窗容器组件 - 使用 WinBox 窗口
*/
import { useEffect, useState, useCallback, useRef } from 'react';
import { Dialog, MessagePlugin, Drawer } from 'tdesign-react';
import { Grid } from 'lucide-react';
import { ATTACHED_ELEMENT_CLASS_NAME } from '@plait/core';
import { MessagePlugin, Drawer } from 'tdesign-react';
import { Images } from 'lucide-react';
import { useAssets } from '../../contexts/AssetContext';
import { MediaLibraryGrid } from './MediaLibraryGrid';
import { MediaLibrarySidebar } from './MediaLibrarySidebar';
import { MediaLibraryInspector } from './MediaLibraryInspector';
import { WinBoxWindow } from '../winbox/WinBoxWindow';
import type {
MediaLibraryModalProps,
Asset,
@@ -257,77 +257,80 @@ export function MediaLibraryModal({
}, [board, removeAsset, assets]);
return (
<Dialog
visible={isOpen}
onClose={onClose}
header={
<div className="media-library-modal__header">
<div className="media-library-modal__header-title">
<div className="media-library-modal__header-title-icon">
<Grid size={16} />
</div>
<span>素材库</span>
<>
<WinBoxWindow
visible={isOpen}
title="素材库"
onClose={onClose}
width="85%"
height="85%"
minWidth={800}
minHeight={500}
x="center"
y="center"
maximizable={true}
minimizable={false}
resizable={true}
movable={true}
modal={false}
className="winbox-media-library"
headerContent={
<div className="media-library-header-content">
<Images size={18} />
</div>
</div>
}
width="80vw"
attach="body"
placement="center"
destroyOnClose
zIndex={5200}
className={`media-library-modal ${ATTACHED_ELEMENT_CLASS_NAME}`}
footer={null}
>
{/* 隐藏的文件输入 */}
<input
ref={fileInputRef}
type="file"
accept="image/*,video/*"
multiple
style={{ display: 'none' }}
onChange={handleFileInputChange}
/>
}
>
{/* 隐藏的文件输入 */}
<input
ref={fileInputRef}
type="file"
accept="image/*,video/*"
multiple
style={{ display: 'none' }}
onChange={handleFileInputChange}
/>
<div className="media-library-layout">
{/* 左侧筛选侧边栏 */}
<div className="media-library-layout__sidebar">
<MediaLibrarySidebar
filters={filters}
assetCount={assets.length}
storageStatus={storageStatus}
onFilterChange={setFilters}
/>
</div>
{/* 中间网格区域 */}
<div className="media-library-layout__main">
<MediaLibraryGrid
filterType={filterType}
selectedAssetId={localSelectedAssetId}
onSelectAsset={handleSelectAsset}
onDoubleClick={handleDoubleClick}
onFileUpload={handleFileUpload}
onUploadClick={handleUploadClick}
/>
</div>
{/* 右侧详情面板 - 仅桌面端显示 */}
{!isMobile && (
<div className="media-library-layout__inspector">
<MediaLibraryInspector
asset={selectedAsset}
onRename={renameAsset}
onDelete={handleRemoveAsset}
onDownload={(asset) => {
downloadFile(asset.url, asset.name);
}}
onSelect={showSelectButton ? handleUseAsset : undefined}
showSelectButton={showSelectButton}
selectButtonText={selectButtonText}
<div className="media-library-layout">
{/* 左侧筛选侧边栏 */}
<div className="media-library-layout__sidebar">
<MediaLibrarySidebar
filters={filters}
assetCount={assets.length}
storageStatus={storageStatus}
onFilterChange={setFilters}
/>
</div>
)}
</div>
{/* 中间网格区域 */}
<div className="media-library-layout__main">
<MediaLibraryGrid
filterType={filterType}
selectedAssetId={localSelectedAssetId}
onSelectAsset={handleSelectAsset}
onDoubleClick={handleDoubleClick}
onFileUpload={handleFileUpload}
onUploadClick={handleUploadClick}
/>
</div>
{/* 右侧详情面板 - 仅桌面端显示 */}
{!isMobile && (
<div className="media-library-layout__inspector">
<MediaLibraryInspector
asset={selectedAsset}
onRename={renameAsset}
onDelete={handleRemoveAsset}
onDownload={(asset) => {
downloadFile(asset.url, asset.name);
}}
onSelect={showSelectButton ? handleUseAsset : undefined}
showSelectButton={showSelectButton}
selectButtonText={selectButtonText}
/>
</div>
)}
</div>
</WinBoxWindow>
{/* 移动端详情抽屉 */}
{isMobile && (
@@ -353,6 +356,6 @@ export function MediaLibraryModal({
/>
</Drawer>
)}
</Dialog>
</>
);
}
@@ -1,12 +1,19 @@
/**
* Media Library Sidebar Styles
* 素材库侧边栏样式 - Tailwind-inspired design
* 素材库侧边栏样式 - 简洁素雅风格
*/
// ========================================
// 品牌色变量(仅用于强调)
// ========================================
$brand-orange: #F39C12;
$brand-orange-light: #FBBF24;
$brand-orange-dark: #E67E22;
.media-library-sidebar {
display: flex;
flex-direction: column;
gap: 16px;
gap: 20px;
height: 100%;
&__section {
@@ -16,52 +23,65 @@
&--storage {
margin-top: auto;
padding-top: 12px;
border-top: 1px solid #e5e7eb;
padding-top: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
}
&__title {
font-size: 10px;
font-size: 11px;
font-weight: 700;
color: #9ca3af;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.08em;
margin: 0;
padding: 0 8px;
padding: 0 10px;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: '';
width: 3px;
height: 12px;
background: $brand-orange;
border-radius: 2px;
}
}
&__buttons {
display: flex;
flex-direction: column;
gap: 3px;
gap: 4px;
.t-button {
justify-content: flex-start;
padding: 7px 8px;
font-size: 12px;
padding: 10px 12px;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
transition: all 0.2s ease;
border-radius: 10px;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
display: flex;
align-items: center;
gap: 12px;
height: auto;
min-height: 32px;
min-height: 40px;
border: 1px solid transparent;
// 图标容器 - 固定大小确保对齐,增加右边距
// 图标容器
.t-icon,
svg {
flex-shrink: 0;
width: 16px;
height: 16px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px !important;
margin-right: 10px !important;
transition: transform 0.2s ease, color 0.2s ease;
}
// 文本 - 确保垂直居中
// 文本
> span {
flex: 1;
text-align: left;
@@ -70,121 +90,151 @@
align-items: center;
}
// 移除 TDesign 默认的间距和 margin
// 移除 TDesign 默认的间距
.t-button__icon {
margin: 0 !important;
}
// 确保内容容器也正确对齐
.t-button__text {
display: flex;
align-items: center;
}
// Active state (base variant)
// 激活状态 (base variant)
&.t-button--variant-base {
background: white;
color: #6366f1;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.05);
border-color: transparent;
background: rgba($brand-orange, 0.1);
color: $brand-orange-dark;
border: 1px solid rgba($brand-orange, 0.2);
svg {
color: $brand-orange;
}
&:hover {
background: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
background: rgba($brand-orange, 0.15);
}
}
// Inactive state (outline variant)
// 非激活状态 (outline variant)
&.t-button--variant-outline {
background: transparent;
color: #6b7280;
color: #64748b;
border-color: transparent;
svg {
color: #94a3b8;
}
&:hover {
background: #f3f4f6;
color: #111827;
background: rgba($brand-orange, 0.06);
color: $brand-orange-dark;
border-color: rgba($brand-orange, 0.1);
transform: translateX(4px);
svg {
color: $brand-orange;
transform: scale(1.1);
}
}
}
}
}
// 移动端优化
// ========== 移动端优化 ==========
@media (max-width: 768px) {
gap: 8px; // 减小间距
gap: 10px;
flex-direction: row;
flex-wrap: wrap;
padding: 8px; // 减小内边距
padding: 8px;
&__section {
gap: 6px; // 减小间距
gap: 6px;
flex: 1 1 auto;
min-width: 120px; // 减小最小宽度
min-width: 120px;
&--storage {
padding-top: 0;
border-top: none;
border-left: 1px solid #e5e7eb;
padding-left: 8px; // 减小内边距
border-left: 1px solid rgba(0, 0, 0, 0.06);
padding-left: 10px;
margin-left: auto;
// 在移动端隐藏存储栏,节省空间
display: none;
}
}
&__title {
font-size: 9px;
margin-bottom: 4px; // 减小标题下方间距
margin-bottom: 4px;
padding: 0 6px;
&::before {
width: 2px;
height: 10px;
}
}
&__buttons {
flex-direction: row;
flex-wrap: wrap;
gap: 4px; // 减小按钮间距
gap: 4px;
.t-button {
padding: 4px 6px; // 减小内边距
font-size: 10px; // 减小字号
min-height: 26px; // 减小高度
gap: 4px; // 减小图标和文字间距
padding: 6px 10px;
font-size: 11px;
min-height: 32px;
gap: 6px;
flex: 1 1 auto;
min-width: 70px; // 减小最小宽度
min-width: 80px;
border-radius: 8px;
.t-icon,
svg {
width: 12px; // 减小图标
height: 12px;
width: 14px;
height: 14px;
margin-right: 6px !important;
}
&.t-button--variant-base:hover,
&.t-button--variant-outline:hover {
transform: none;
}
}
}
}
@media (max-width: 480px) {
padding: 6px; // 进一步减小
padding: 6px;
&__section {
min-width: 100px; // 更小
min-width: 100px;
gap: 4px;
}
&__title {
font-size: 8px;
margin-bottom: 3px;
&::before {
display: none;
}
}
&__buttons {
gap: 3px;
.t-button {
padding: 3px 5px; // 更小的内边距
font-size: 9px; // 更小字号
min-height: 24px; // 更小高度
min-width: 60px; // 更小宽度
gap: 3px;
padding: 4px 8px;
font-size: 10px;
min-height: 28px;
min-width: 70px;
gap: 4px;
border-radius: 6px;
.t-icon,
svg {
width: 10px; // 更小图标
height: 10px;
width: 12px;
height: 12px;
margin-right: 4px !important;
}
}
}
@@ -1,17 +1,20 @@
/**
* Storage Bar Styles
* 存储空间进度条样式
* 存储空间进度条样式 - 简洁素雅风格
*/
// 品牌色变量(仅用于强调)
$brand-orange: #F39C12;
.storage-bar {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 16px;
gap: 8px;
padding: 14px 16px;
background: white;
border-radius: 8px;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.03);
&__header {
display: flex;
@@ -22,25 +25,30 @@
&__header-left {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
}
&__icon {
flex-shrink: 0;
color: #6b7280;
color: $brand-orange;
width: 16px;
height: 16px;
}
&__title {
font-size: 11px;
font-size: 12px;
font-weight: 600;
color: #111827;
color: #1e293b;
}
&__count {
color: #6b7280;
color: #64748b;
font-weight: 500;
font-size: 11px;
white-space: nowrap;
background: rgba($brand-orange, 0.08);
padding: 2px 8px;
border-radius: 4px;
}
&__usage {
@@ -50,45 +58,62 @@
}
&__usage-value {
font-size: 12px;
color: #111827;
font-size: 13px;
color: #1e293b;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
letter-spacing: -0.5px;
font-weight: 600;
}
// 进度条容器样式调整
.t-progress {
margin: 0;
// 自定义进度条百分比显示样式
// 进度条轨道
.t-progress__bar {
background: #f1f5f9;
border-radius: 6px;
overflow: hidden;
}
// 进度条填充 - 单色
.t-progress__inner {
background: $brand-orange !important;
border-radius: 6px;
transition: width 0.3s ease;
}
// 百分比显示
&__info {
font-size: 11px;
font-weight: 600;
color: #111827;
font-weight: 700;
color: $brand-orange;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
}
}
// 移动端优化
@media (max-width: 768px) {
padding: 10px 14px;
padding: 12px 14px;
gap: 6px;
border-radius: 10px;
&__icon {
width: 12px;
height: 12px;
width: 14px;
height: 14px;
}
&__title {
font-size: 10px;
font-size: 11px;
}
&__count {
font-size: 10px;
padding: 2px 6px;
}
&__usage-value {
font-size: 16px;
font-size: 12px;
}
}
}
@@ -1,38 +1,46 @@
/**
* View Mode Toggle Styles
* 视图模式切换样式
* 视图模式切换样式 - 简洁素雅风格
*/
// 品牌色变量
$brand-orange: #F39C12;
.view-mode-toggle {
display: flex;
align-items: center;
background: #f3f4f6;
border-radius: 6px;
padding: 2px;
background: #f1f5f9;
border-radius: 8px;
padding: 3px;
gap: 2px;
&__btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
width: 30px;
height: 30px;
border: none;
background: transparent;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
color: #6b7280;
color: #94a3b8;
transition: all 0.15s ease;
svg {
width: 16px;
height: 16px;
}
&:hover:not(&--active) {
background: rgba(0, 0, 0, 0.05);
color: #374151;
background: rgba(0, 0, 0, 0.04);
color: #64748b;
}
&--active {
background: white;
color: #6366f1;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
color: $brand-orange;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
}
@@ -1,10 +1,14 @@
/**
* Virtual Asset Grid Styles
* 虚拟滚动网格样式
* 虚拟滚动网格样式 - 简洁素雅风格
*/
.virtual-asset-grid {
background: #fafbfc;
background: #f8fafc;
transition: opacity 0.2s ease;
flex: 1;
min-height: 0;
height: 100%;
// 网格模式
&--grid {
@@ -15,7 +19,6 @@
> .asset-item {
flex: 0 0 auto;
// 宽度由 JS 动态计算,这里只设置基础样式
}
}
}
@@ -35,6 +38,8 @@
// 列表模式
&--list {
background: #ffffff;
.virtual-asset-grid__row {
display: block;
@@ -50,8 +55,33 @@
&--grid,
&--compact {
// 最后一行可能不满,需要让元素左对齐
justify-content: flex-start;
}
}
// 滚动容器样式
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 4px;
border: 2px solid transparent;
background-clip: content-box;
&:hover {
background: #cbd5e1;
background-clip: content-box;
}
}
&::-webkit-scrollbar-corner {
background: transparent;
}
}
@@ -116,6 +116,9 @@ export interface WinBoxWindowProps {
/**
* WinBox 窗口 React 封装组件
* 提供可拖拽、可调整大小、可最小化/最大化的浮动窗口体验
*
* 重要:使用 React Portal 渲染内容到 WinBox 的 .wb-body 中,
* 而不是使用 WinBox 的 mount 选项,以保持 React 事件系统正常工作。
*/
export const WinBoxWindow: React.FC<WinBoxWindowProps> = ({
visible,
@@ -148,9 +151,9 @@ export const WinBoxWindow: React.FC<WinBoxWindowProps> = ({
autoMaximize = false,
}) => {
const winboxRef = useRef<any>(null);
const contentRef = useRef<HTMLDivElement>(null);
const winboxElementRef = useRef<HTMLDivElement | null>(null); // WinBox 窗口的 DOM 元素
const [headerPortalContainer, setHeaderPortalContainer] = useState<HTMLElement | null>(null);
const [bodyPortalContainer, setBodyPortalContainer] = useState<HTMLElement | null>(null);
const [isReady, setIsReady] = useState(false);
const [winboxLoaded, setWinboxLoaded] = useState(!!WinBoxConstructor);
@@ -189,13 +192,14 @@ export const WinBoxWindow: React.FC<WinBoxWindowProps> = ({
winboxRef.current = null;
winboxElementRef.current = null; // 清空 DOM 元素引用
setHeaderPortalContainer(null);
setBodyPortalContainer(null);
setIsReady(false);
}
return;
}
// 当 visible 变为 true 且窗口不存在时,创建窗口
if (visible && !winboxRef.current && contentRef.current) {
if (visible && !winboxRef.current) {
// 构建 class 列表
const classList: string[] = ['winbox-react'];
if (!maximizable) classList.push('no-max');
@@ -206,11 +210,11 @@ export const WinBoxWindow: React.FC<WinBoxWindowProps> = ({
classList.push('no-full');
if (className) classList.push(className);
// 创建 WinBox 实例
// 创建 WinBox 实例 - 不使用 mount 选项,改用 Portal
const wb = new WinBoxConstructor({
id,
title,
mount: contentRef.current,
// 不使用 mount,改用 React Portal 渲染内容
width,
height,
minwidth: minWidth,
@@ -240,6 +244,12 @@ export const WinBoxWindow: React.FC<WinBoxWindowProps> = ({
requestAnimationFrame(() => {
refreshViewportScale();
});
// 获取 .wb-body 元素作为内容的 Portal 容器
const wbBody = wb.window.querySelector('.wb-body');
if (wbBody) {
setBodyPortalContainer(wbBody as HTMLElement);
}
}
// 如果有自定义标题栏内容,创建 portal 容器
@@ -316,24 +326,17 @@ export const WinBoxWindow: React.FC<WinBoxWindowProps> = ({
}
}, [autoMaximize]);
// 渲染内容容器(WinBox 会挂载这个元素)
// 使用 Portal 渲染内容到 WinBox 的 .wb-body 中
// 这样可以保持 React 事件系统正常工作
return (
<>
{/*
创建一个持久的占位容器 div。
当 WinBox.mount() 把 contentRef 移走时,这个占位容器仍然留在原来的 DOM 位置。
这样 React 在进行兄弟节点插入(如 insertBefore)时,仍然能找到正确的参考节点,
避免 "The node before which the new node is to be inserted is not a child of this node" 错误。
*/}
<div style={{ display: 'none' }} data-winbox-placeholder="true">
<div
ref={contentRef}
className="winbox-content-wrapper"
style={{ display: visible ? 'block' : 'none' }}
>
{/* 内容通过 Portal 渲染到 WinBox 的 .wb-body */}
{isReady && bodyPortalContainer && createPortal(
<div className="winbox-content-wrapper">
{children}
</div>
</div>
</div>,
bodyPortalContainer
)}
{/* 自定义标题栏内容通过 Portal 渲染 */}
{isReady && headerContent && headerPortalContainer &&
createPortal(headerContent, headerPortalContainer)