docs(claude): 添加 React 事件委托、筛选逻辑、Grid 布局及 UI 高度统一规则
This commit is contained in:
1 parent
e4cdca4515
commit
dcabd75e69
16 files changed
+1300
-721
No files matched your search
@@ -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` 对大型组件进行代码分割
|
||||
- 对图片实现懒加载和预加载
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
// 完整内存显示
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* 内存日志行内信息 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user