docs(claude): 添加新规则 - Freehand 元素属性设置需要自定义 callback

This commit is contained in:
liquidliang committed 2026-01-14 17:16:04 +08:00
1 parent 3ad84006e3
commit e27bf28ec3
10 files changed
+99 -22

No files matched your search

+27
View File
@@ -1406,6 +1406,33 @@ function applyCursorStyle(board: PlaitBoard, size: number) {
**检查方法**: 如果 JavaScript 设置的样式不生效,在浏览器开发者工具中检查元素样式,查看是否有 `!important` 规则覆盖。
### Freehand 元素属性设置需要自定义 callback
**场景**: 修改 Freehand(手绘线条)元素的属性(如 strokeStyle、strokeColor)时
❌ **错误示例**:
```typescript
// 错误:直接使用 PropertyTransforms,Freehand 元素可能不被正确处理
const setStrokeStyle = (style: StrokeStyle) => {
PropertyTransforms.setStrokeStyle(board, style, { getMemorizeKey });
};
```
✅ **正确示例**:
```typescript
// 正确:使用 callback 确保所有选中元素都被处理
export const setStrokeStyle = (board: PlaitBoard, strokeStyle: StrokeStyle) => {
PropertyTransforms.setStrokeStyle(board, strokeStyle, {
getMemorizeKey,
callback: (element: PlaitElement, path: Path) => {
Transforms.setNode(board, { strokeStyle }, path);
},
});
};
```
**原因**: `PropertyTransforms` 的默认行为可能不会处理所有类型的元素(如自定义的 Freehand 元素)。通过提供 `callback` 函数,可以确保对所有选中的元素执行属性设置操作。颜色设置(`setStrokeColor`、`setFillColor`)也使用了相同的模式。
### 性能指南
- 使用 `React.lazy` 对大型组件进行代码分割
- 对图片实现懒加载和预加载
@@ -71,7 +71,7 @@ export const PopupFillButton: React.FC<PopupFillButtonProps> = ({
<PopoverContent container={container}>
<Island
padding={4}
className={classNames(`${ATTACHED_ELEMENT_CLASS_NAME}`)}
className={classNames(`${ATTACHED_ELEMENT_CLASS_NAME}`, 'fill-setting')}
>
<UnifiedColorPicker
value={currentColor}
@@ -41,4 +41,15 @@
justify-content: space-between;
padding: 0 8px;
}
// 限制拾色器宽度与文字设置面板一致
.unified-color-picker {
width: 280px;
}
}
// 填充拾色器宽度限制
.fill-setting {
.unified-color-picker {
width: 280px;
}
}
@@ -44,7 +44,17 @@ export const RecentColorsProvider: React.FC<RecentColorsProviderProps> = ({ chil
kvStorageService.get<string[]>(RECENT_COLORS_KEY)
.then((colors) => {
if (colors && Array.isArray(colors)) {
setRecentColors(colors);
// 去重:使用 Set 并标准化颜色格式
const seen = new Set<string>();
const uniqueColors = colors.filter((color) => {
const normalized = color.toUpperCase().replace(/\s/g, '');
if (seen.has(normalized)) {
return false;
}
seen.add(normalized);
return true;
});
setRecentColors(uniqueColors);
}
})
.catch((error) => {
@@ -21,6 +21,7 @@ import {
setAlphaToHex,
removeAlphaFromHex,
isValidHex,
isLightColor,
} from './utils';
import './unified-color-picker.scss';
@@ -42,6 +43,9 @@ export const UnifiedColorPicker: React.FC<UnifiedColorPickerProps> = ({
}) => {
// 最近使用颜色
const { recentColors, addRecentColor } = useRecentColors();
// 缓存首次渲染时的最近使用颜色,避免选择颜色时顺序变化
const [initialRecentColors] = useState(() => [...recentColors]);
// 解析初始颜色
const initialColor = value && isValidHex(value) ? value : DEFAULT_COLOR;
@@ -201,23 +205,27 @@ export const UnifiedColorPicker: React.FC<UnifiedColorPickerProps> = ({
)}
{/* 最近使用颜色 */}
{showRecentColors && recentColors.length > 0 && (
{showRecentColors && initialRecentColors.length > 0 && (
<div className="unified-color-picker__recent">
<div className="unified-color-picker__recent-label">最近使用</div>
<div className="unified-color-picker__recent-colors">
{recentColors.map((color, index) => (
<button
key={`${color}-${index}`}
className={classNames('unified-color-picker__recent-item', {
'unified-color-picker__recent-item--selected':
removeAlphaFromHex(color).toUpperCase() === currentHex.toUpperCase(),
})}
style={{ backgroundColor: color }}
onClick={() => handleRecentSelect(color)}
disabled={disabled}
title={color}
/>
))}
{initialRecentColors.map((color, index) => {
const isLight = isLightColor(color);
return (
<button
key={`${color}-${index}`}
className={classNames('unified-color-picker__recent-item', {
'unified-color-picker__recent-item--selected':
removeAlphaFromHex(color).toUpperCase() === currentHex.toUpperCase(),
'unified-color-picker__recent-item--light': isLight,
})}
style={{ backgroundColor: color }}
onClick={() => handleRecentSelect(color)}
disabled={disabled}
title={color}
/>
);
})}
</div>
</div>
)}
@@ -3,7 +3,7 @@
*/
// 变量 - 优化紧凑布局
$ucp-panel-width: 220px;
$ucp-panel-min-width: 200px;
$ucp-panel-height: 140px;
$ucp-slider-height: 10px;
$ucp-indicator-size: 12px;
@@ -22,11 +22,13 @@ $checkerboard-bg: repeating-conic-gradient(
display: flex;
flex-direction: column;
gap: $ucp-gap;
width: $ucp-panel-width;
width: 100%;
min-width: $ucp-panel-min-width;
padding: 8px;
background: var(--color-surface, #ffffff);
border-radius: $ucp-border-radius;
user-select: none;
box-sizing: border-box;
&.disabled {
opacity: 0.6;
@@ -173,6 +175,10 @@ $checkerboard-bg: repeating-conic-gradient(
background: $checkerboard-bg !important;
border: 1px solid rgba(0, 0, 0, 0.15);
}
&--light {
border: 1px solid rgba(0, 0, 0, 0.15);
}
}
&__no-color-icon {
@@ -207,10 +213,12 @@ $checkerboard-bg: repeating-conic-gradient(
display: flex;
flex-wrap: nowrap;
gap: 4px;
height: $ucp-recent-size;
overflow: hidden;
}
&-item {
flex-shrink: 0;
width: $ucp-recent-size;
height: $ucp-recent-size;
padding: 0;
@@ -226,6 +234,10 @@ $checkerboard-bg: repeating-conic-gradient(
&--selected {
box-shadow: 0 0 0 2px var(--color-primary, #1890ff);
}
&--light {
border: 1px solid rgba(0, 0, 0, 0.15);
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { DEFAULT_COLOR } from '@plait/core';
import { NO_COLOR, WHITE } from '@aitu/utils';
export const CLASSIC_COLORS = [
{ name: 'Topic Color', value: NO_COLOR },
{ name: '无色', value: NO_COLOR },
{ name: 'Basic Black', value: DEFAULT_COLOR },
{ name: 'White', value: WHITE },
{ name: 'Gray', value: '#808080' },
@@ -189,6 +189,10 @@ export const getStrokeColorByElement = (
board: PlaitBoard,
element: PlaitElement
) => {
// 如果明确设置为 'none',返回 'none'(无色)
if (element.strokeColor === 'none') {
return 'none';
}
const defaultColor = getFreehandDefaultStrokeColor(
board.theme.themeColorMode
);
@@ -197,6 +201,10 @@ export const getStrokeColorByElement = (
};
export const getFillByElement = (board: PlaitBoard, element: PlaitElement) => {
// 如果明确设置为 'none',返回 'none'(无色)
if (element.fill === 'none') {
return 'none';
}
const defaultFill =
Freehand.isFreehand(element) && isClosedCustomGeometry(board, element)
? getFreehandDefaultFill(board.theme.themeColorMode)
+2 -2
View File
@@ -54,7 +54,7 @@ export const setFillColor = (board: PlaitBoard, fillColor: string) => {
const currentFill = getCurrentFill(board, element);
const currentOpacity = hexAlphaToOpacity(currentFill);
if (isNoColor(fillColor)) {
Transforms.setNode(board, { fill: null }, path);
Transforms.setNode(board, { fill: 'none' }, path);
} else {
if (
isNullOrUndefined(currentOpacity) ||
@@ -97,7 +97,7 @@ export const setStrokeColor = (board: PlaitBoard, newColor: string) => {
const currentStrokeColor = getCurrentStrokeColor(board, element);
const currentOpacity = hexAlphaToOpacity(currentStrokeColor);
if (isNoColor(newColor)) {
Transforms.setNode(board, { strokeColor: null }, path);
Transforms.setNode(board, { strokeColor: 'none' }, path);
} else {
if (
isNullOrUndefined(currentOpacity) ||
+2 -1
View File
@@ -24,7 +24,8 @@ export const isClosedElement = (board: PlaitBoard, element: PlaitElement) => {
return (
MindElement.isMindElement(board, element) ||
(PlaitDrawElement.isDrawElement(element) && isClosedDrawElement(element)) ||
isClosedCustomGeometry(board, element)
isClosedCustomGeometry(board, element) ||
Freehand.isFreehand(element)
);
};