feat(ui): 统一 Tooltip 样式并美化模型健康状态信号展示
- 统一全站 Tooltip 为白底样式 (theme="light"),并优化层级 (zIndex) - 将模型健康状态由圆点改为 3 格信号塔设计,根据错误率 (errorRate) 自动调整格数与颜色 - 在模型下拉选择器触发按钮上增加实时健康状态展示 - 更新 CLAUDE.md 和 CODING_RULES.md,添加 Tooltip 规范与量化表意设计准则
This commit is contained in:
1 parent
f110657f6d
commit
1076cb2411
13 files changed
+206
-71
No files matched your search
@@ -194,7 +194,8 @@ import { quickInsert } from '../../../services/canvas-operations';
|
||||
|
||||
### UI 规范
|
||||
|
||||
- **Tooltips**:始终使用 `theme='light'`
|
||||
- **Tooltips**:始终使用 `theme='light'`,高层级容器内需显式设置更高 `zIndex` (如 20000)
|
||||
- **状态表意**:优先使用量化组件(如信号格)而非单一颜色圆点来展示程度差异
|
||||
- **按钮圆角**:8px
|
||||
- **卡片圆角**:12px
|
||||
- **动画时长**:150-300ms,ease-out 曲线
|
||||
|
||||
@@ -3936,6 +3936,63 @@ const matchesSource = !filters.activeSource || filters.activeSource === 'ALL' ||
|
||||
|
||||
## UI 交互规范
|
||||
|
||||
#### Tooltip 样式统一规范
|
||||
|
||||
**场景**: 在项目中使用 TDesign 的 `Tooltip` 组件时。
|
||||
|
||||
❌ **错误示例**:
|
||||
```tsx
|
||||
<Tooltip content="提示文字">
|
||||
<Button icon={<Icon />} />
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
✅ **正确示例**:
|
||||
```tsx
|
||||
<Tooltip content="提示文字" theme="light" showArrow={false}>
|
||||
<Button icon={<Icon />} />
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
**原因**: 为了保持项目视觉风格的高度统一,所有 Tooltip 必须使用 `theme="light"`(白底黑字)。同时,为了界面更简洁,推荐在图标按钮或紧凑列表项上使用 `showArrow={false}` 隐藏箭头。
|
||||
|
||||
#### 高层级容器中的 Tooltip 遮挡问题
|
||||
|
||||
**场景**: 在使用 `createPortal` 渲染的弹窗、下拉菜单或设置了极高 `zIndex` 的容器内部使用 `Tooltip` 时。
|
||||
|
||||
❌ **错误示例**:
|
||||
```tsx
|
||||
// 在 zIndex: 10000 的下拉菜单中
|
||||
<Tooltip content="状态提示">
|
||||
<div className="status-dot" />
|
||||
</Tooltip>
|
||||
// 结果:Tooltip 被挡在下拉菜单下面,看不见
|
||||
```
|
||||
|
||||
✅ **正确示例**:
|
||||
```tsx
|
||||
<Tooltip content="状态提示" theme="light" zIndex={20000}>
|
||||
<div className="status-dot" />
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
**原因**: 项目中部分浮层(如模型选择下拉)使用了 `createPortal` 且 `zIndex` 达到 10000。默认层级的 `Tooltip` 会被遮挡。在这种情况下,必须显式将 `Tooltip` 的 `zIndex` 提升到更高(如 20000)以确保可见。
|
||||
|
||||
#### 信号/状态展示的量化表意
|
||||
|
||||
**场景**: 展示模型健康度、网络信号等需要量化感知的状态时。
|
||||
|
||||
❌ **错误示例**:
|
||||
使用单一圆点或方块,仅靠颜色区分。用户难以感知“程度”的差异。
|
||||
|
||||
✅ **正确示例**:
|
||||
使用“信号格”或“进度条”设计,配合颜色变化。
|
||||
- 3 格绿色:极佳
|
||||
- 2 格橙色:一般
|
||||
- 1 格红色:极差
|
||||
|
||||
**原因**: 相比单一的圆点,信号格能更直观地传达“量”的概念,符合用户的直觉认知(如手机信号、WiFi 强度)。
|
||||
|
||||
### 可点击容器模式:扩大交互区域
|
||||
|
||||
**场景**: 当 checkbox、按钮等小型交互元素嵌套在容器中时,用户期望点击整个容器都能触发操作。
|
||||
|
||||
@@ -242,6 +242,7 @@ export const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||
>
|
||||
<span className="model-dropdown__at">#</span>
|
||||
<span className="model-dropdown__code">{shortCode}</span>
|
||||
<ModelHealthBadge modelId={selectedModel} />
|
||||
<ChevronDown size={14} className={`model-dropdown__chevron ${isOpen ? 'model-dropdown__chevron--open' : ''}`} />
|
||||
</button>
|
||||
);
|
||||
@@ -258,6 +259,7 @@ export const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||
}}
|
||||
>
|
||||
<div className="model-dropdown__form-content">
|
||||
<ModelHealthBadge modelId={selectedModel} />
|
||||
<input
|
||||
ref={triggerInputRef}
|
||||
type="text"
|
||||
|
||||
@@ -11,6 +11,7 @@ $gray-medium: #9CA3AF;
|
||||
$gray-dark: #6B7280;
|
||||
$text-dark: #1F2937;
|
||||
$vip-color: #E91E63;
|
||||
$green-health: #10B981;
|
||||
|
||||
.model-dropdown {
|
||||
position: relative;
|
||||
@@ -36,29 +37,24 @@ $vip-color: #E91E63;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
position: relative;
|
||||
|
||||
&--minimal {
|
||||
gap: 1px;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba($orange-primary, 0.1);
|
||||
.model-dropdown__at {
|
||||
color: $orange-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.model-dropdown__trigger--open {
|
||||
background: rgba($orange-primary, 0.15);
|
||||
.model-dropdown__code {
|
||||
color: $gray-dark;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,15 +65,21 @@ $vip-color: #E91E63;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
min-height: 36px;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: $orange-primary;
|
||||
.model-dropdown__form-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.model-dropdown__trigger--open {
|
||||
border-color: $orange-primary;
|
||||
box-shadow: 0 0 0 2px rgba($orange-primary, 0.1);
|
||||
}
|
||||
// 触发器上的健康状态指示
|
||||
.model-health-signal {
|
||||
margin-left: -2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export const CharacterSelector: React.FC<CharacterSelectorProps> = ({
|
||||
<Tooltip
|
||||
content="从已完成的 Sora-2 视频中提取角色后可在此选择"
|
||||
theme="light"
|
||||
showArrow={false}
|
||||
>
|
||||
<InfoCircleIcon style={{ marginLeft: 4, cursor: 'help' }} />
|
||||
</Tooltip>
|
||||
@@ -111,6 +112,7 @@ export const CharacterSelector: React.FC<CharacterSelectorProps> = ({
|
||||
key={character.id}
|
||||
content={character.sourcePrompt || `@${character.username}`}
|
||||
theme="light"
|
||||
showArrow={false}
|
||||
>
|
||||
<div
|
||||
className={`character-selector__chip ${selectedIds.includes(character.id) ? 'character-selector__chip--selected' : ''}`}
|
||||
@@ -133,6 +135,7 @@ export const CharacterSelector: React.FC<CharacterSelectorProps> = ({
|
||||
<Tooltip
|
||||
content={`还有 ${completedCharacters.length - maxVisible} 个角色`}
|
||||
theme="light"
|
||||
showArrow={false}
|
||||
>
|
||||
<div className="character-selector__chip">
|
||||
+{completedCharacters.length - maxVisible}
|
||||
|
||||
@@ -780,13 +780,13 @@ export function MediaLibraryGrid({
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={group.id}
|
||||
content={`${group.label}: ${isAsc ? '正序' : (isDesc ? '逆序' : '默认')}`}
|
||||
placement="top"
|
||||
theme="light"
|
||||
showArrow={false}
|
||||
>
|
||||
<Tooltip
|
||||
key={group.id}
|
||||
content={`${group.label}: ${isAsc ? '正序' : (isDesc ? '逆序' : '默认')}`}
|
||||
placement="top"
|
||||
theme="light"
|
||||
showArrow={false}
|
||||
>
|
||||
<div
|
||||
className={`media-library-grid__filter-option ${isActive ? 'media-library-grid__filter-option--active' : ''}`}
|
||||
onClick={handleSortClick}
|
||||
|
||||
@@ -91,6 +91,7 @@ export const SettingsDialog = ({
|
||||
}
|
||||
placement="top"
|
||||
theme='light'
|
||||
showArrow={false}
|
||||
>
|
||||
<InfoCircleIcon className="settings-dialog__tooltip-icon" />
|
||||
</Tooltip>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'tdesign-react';
|
||||
import { useModelHealth } from '../../hooks/useModelHealth';
|
||||
import './model-health-badge.scss';
|
||||
|
||||
@@ -37,14 +38,53 @@ export const ModelHealthBadge: React.FC<ModelHealthBadgeProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { statusLabel, statusColor } = status;
|
||||
const { statusLabel, statusColor, errorRate } = status;
|
||||
|
||||
// 计算信号格数 (0-3格) 和 颜色
|
||||
// errorRate 是 0-100
|
||||
let bars = 3;
|
||||
let barColor = statusColor;
|
||||
|
||||
if (errorRate > 80) {
|
||||
bars = 1;
|
||||
barColor = '#991B1B'; // 深红色
|
||||
} else if (errorRate > 50) {
|
||||
bars = 1;
|
||||
barColor = '#EF4444'; // 红色
|
||||
} else if (errorRate > 20) {
|
||||
bars = 2;
|
||||
barColor = '#C2410C'; // 深橙色
|
||||
} else if (errorRate >= 1) {
|
||||
bars = 2;
|
||||
barColor = '#F97316'; // 浅橙色
|
||||
} else {
|
||||
bars = 3;
|
||||
barColor = '#10B981'; // 绿色 (0 或 接近 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`model-health-badge ${className}`}
|
||||
title={statusLabel}
|
||||
style={{ backgroundColor: statusColor }}
|
||||
/>
|
||||
<Tooltip
|
||||
content={errorRate > 0 ? `${statusLabel} (拥挤度:${errorRate}%)` : statusLabel}
|
||||
theme="light"
|
||||
placement="top"
|
||||
showArrow={false}
|
||||
zIndex={20000}
|
||||
>
|
||||
<div className={`model-health-signal ${className}`}>
|
||||
<div
|
||||
className={`model-health-signal__bar ${bars >= 1 ? 'active' : ''}`}
|
||||
style={{ backgroundColor: bars >= 1 ? barColor : undefined }}
|
||||
/>
|
||||
<div
|
||||
className={`model-health-signal__bar ${bars >= 2 ? 'active' : ''}`}
|
||||
style={{ backgroundColor: bars >= 2 ? barColor : undefined }}
|
||||
/>
|
||||
<div
|
||||
className={`model-health-signal__bar ${bars >= 3 ? 'active' : ''}`}
|
||||
style={{ backgroundColor: bars >= 3 ? barColor : undefined }}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -337,6 +337,7 @@ export const MediaViewport: React.FC<MediaViewportProps> = ({
|
||||
content={toolbarOrientation === 'horizontal' ? '切换为垂直布局' : '切换为水平布局'}
|
||||
theme="light"
|
||||
placement="top"
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -65,6 +65,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className="viewer-toolbar__btn"
|
||||
@@ -86,6 +87,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className="viewer-toolbar__btn"
|
||||
@@ -108,6 +110,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className={`viewer-toolbar__btn viewer-toolbar__btn--small ${
|
||||
@@ -130,6 +133,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className={`viewer-toolbar__btn ${
|
||||
@@ -145,6 +149,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className={`viewer-toolbar__btn ${
|
||||
@@ -160,6 +165,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className={`viewer-toolbar__btn ${
|
||||
@@ -180,6 +186,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className={`viewer-toolbar__btn ${
|
||||
@@ -198,6 +205,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button className="viewer-toolbar__btn" onClick={onResetView}>
|
||||
<RotateCcw size={18} />
|
||||
@@ -221,6 +229,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button className="viewer-toolbar__btn" onClick={onFullscreen}>
|
||||
<Maximize2 size={18} />
|
||||
@@ -234,6 +243,7 @@ export const ViewerToolbar: React.FC<ViewerToolbarProps> = ({
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
zIndex={TOOLTIP_Z_INDEX}
|
||||
showArrow={false}
|
||||
>
|
||||
<button
|
||||
className="viewer-toolbar__btn viewer-toolbar__btn--close"
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
/**
|
||||
* 模型健康状态徽章样式
|
||||
* 固定大小的彩色方块
|
||||
* 模型健康状态信号样式
|
||||
* 3格信号塔设计
|
||||
*/
|
||||
|
||||
.model-health-badge {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
cursor: help;
|
||||
$gray-light: #e5e7eb;
|
||||
|
||||
// 微弱的发光效果增加辨识度
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
|
||||
.model-health-signal {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
gap: 1.5px;
|
||||
height: 10px;
|
||||
padding: 0 2px;
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__bar {
|
||||
width: 2.5px;
|
||||
background-color: $gray-light;
|
||||
border-radius: 0.5px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:nth-child(1) { height: 40%; }
|
||||
&:nth-child(2) { height: 70%; }
|
||||
&:nth-child(3) { height: 100%; }
|
||||
|
||||
&.active {
|
||||
// 背景色由 inline style 控制
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在 ModelDropdown 列表项中的位置
|
||||
.model-dropdown__item-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
|
||||
.model-health-badge {
|
||||
margin-left: 4px;
|
||||
.model-health-signal {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 ModelSelector 列表项中的位置
|
||||
.ai-model-selector__item-name {
|
||||
.model-health-badge {
|
||||
.model-health-signal {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
@@ -38,10 +53,10 @@
|
||||
.ai-input-bar__at-suggestion-item-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
|
||||
.model-health-badge {
|
||||
margin-left: 4px;
|
||||
.model-health-signal {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2081,18 +2081,19 @@ const BatchImageGeneration: React.FC<BatchImageGenerationProps> = ({ onSwitchToS
|
||||
{rowInfo.tasks[0].error.message}
|
||||
</span>
|
||||
{rowInfo.tasks[0].error.details?.originalError && (
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="error-details-tooltip">
|
||||
<div className="error-details-title">原始错误信息:</div>
|
||||
<div className="error-details-content">
|
||||
{rowInfo.tasks[0].error.details.originalError}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="error-details-tooltip">
|
||||
<div className="error-details-title">原始错误信息:</div>
|
||||
<div className="error-details-content">
|
||||
{rowInfo.tasks[0].error.details.originalError}
|
||||
</div>
|
||||
}
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
>
|
||||
</div>
|
||||
}
|
||||
theme="light"
|
||||
placement="bottom"
|
||||
showArrow={false}
|
||||
>
|
||||
<span className="preview-error-details-link">[详情]</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -74,12 +74,13 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
||||
<div className="aspect-ratio-selector__dropdown">
|
||||
<div className="aspect-ratio-selector__dropdown-grid">
|
||||
{ASPECT_RATIO_OPTIONS.map((option) => (
|
||||
<Tooltip
|
||||
key={option.value}
|
||||
content={option.description}
|
||||
theme="light"
|
||||
placement="top"
|
||||
>
|
||||
<Tooltip
|
||||
key={option.value}
|
||||
content={option.description}
|
||||
theme="light"
|
||||
placement="top"
|
||||
showArrow={false}
|
||||
>
|
||||
<div
|
||||
className={`aspect-ratio-selector__dropdown-item ${
|
||||
value === option.value ? 'aspect-ratio-selector__dropdown-item--selected' : ''
|
||||
@@ -112,6 +113,7 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
||||
content={option.description}
|
||||
theme="light"
|
||||
placement="top"
|
||||
showArrow={false}
|
||||
>
|
||||
<div
|
||||
className={`aspect-ratio-selector__option ${
|
||||
|
||||
Reference in new issue
Block a user