feat(ui): 统一 Tooltip 样式并美化模型健康状态信号展示

- 统一全站 Tooltip 为白底样式 (theme="light"),并优化层级 (zIndex)
- 将模型健康状态由圆点改为 3 格信号塔设计,根据错误率 (errorRate) 自动调整格数与颜色
- 在模型下拉选择器触发按钮上增加实时健康状态展示
- 更新 CLAUDE.md 和 CODING_RULES.md,添加 Tooltip 规范与量化表意设计准则
This commit is contained in:
liquid committed 2026-01-22 22:38:58 +08:00
1 parent f110657f6d
commit 1076cb2411
13 files changed
+206 -71

No files matched your search

+2 -1
View File
@@ -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 曲线
+57
View File
@@ -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 ${