Files
aida_front/src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue

1066 lines
26 KiB
Vue
Raw Normal View History

2025-06-09 10:25:54 +08:00
<!-- filepath: /Users/aaron/work/pc/air/canvasEdit/src/components/CanvasEditor/components/TextEditorPanel.vue -->
<template>
<div v-if="visible" class="text-editor-panel" :class="{ 'is-active': visible }">
2025-06-09 10:25:54 +08:00
<div class="text-editor-panel-header">
<div class="header-btn import-btn">编辑文本样式</div>
2025-06-09 10:25:54 +08:00
<div class="header-actions">
<div class="header-btn cancel-btn" @click="close">取消</div>
<div class="header-btn confirm-btn" @click="confirmEdit">完成</div>
2025-06-09 10:25:54 +08:00
</div>
</div>
<div class="text-editor-content">
<!-- 字体选择列表 -->
<div class="edit-column font-column">
<div class="column-header">字体</div>
<div class="font-list">
<div
v-for="font in availableFonts"
:key="font.value"
class="font-item"
:class="{ active: fontFamily === font.value }"
@click="selectFont(font.value)"
>
{{ font.label }}
</div>
</div>
</div>
<!-- 样式选择区域 -->
<div class="edit-column style-column">
<div class="column-header">样式</div>
<div class="style-preview">
<div class="style-name">Regular</div>
<div class="style-sample" :style="{ fontFamily }">Regular</div>
</div>
</div>
<!-- 设计参数区域 -->
<div class="edit-column design-column">
<div class="column-header">设计</div>
<div class="param-item">
<div class="param-label">尺寸</div>
<div class="param-control">
<input
type="range"
v-model.number="fontSize"
min="1"
max="200"
@input="updateFontSize"
class="slider-control"
/>
<div class="param-value">{{ fontSize }}pt</div>
</div>
</div>
<div class="param-item">
<div class="param-label">字符间距</div>
<div class="param-control">
<input
type="range"
v-model.number="charSpacingPercent"
min="-50"
max="100"
step="0.1"
@input="updateCharSpacing"
class="slider-control"
/>
<div class="param-value">{{ charSpacingPercent.toFixed(1) }}%</div>
</div>
</div>
<div class="param-item">
<div class="param-label">行距</div>
<div class="param-control">
<input
type="range"
v-model.number="lineHeight"
min="0.5"
max="3"
step="0.01"
@input="updateLineHeight"
class="slider-control"
/>
<div class="param-value">{{ lineHeightToPt }}pt</div>
</div>
</div>
<!-- <div class="param-item">
2025-06-09 10:25:54 +08:00
<div class="param-label">基线</div>
<div class="param-control">
<input
type="range"
v-model.number="baseline"
min="-20"
max="20"
@input="updateBaseline"
class="slider-control"
/>
<div class="param-value">{{ baseline }}pt</div>
</div>
</div> -->
2025-06-09 10:25:54 +08:00
<div class="param-item">
<div class="param-label">不透明度</div>
<div class="param-control">
<input
type="range"
v-model.number="opacity"
min="0.1"
max="1"
step="0.01"
@input="updateOpacity"
class="slider-control"
/>
<div class="param-value">{{ (opacity * 100).toFixed(1) }}%</div>
</div>
</div>
</div>
<!-- 字体属性区域 -->
<div class="edit-column props-column">
<div class="column-header">字体属性</div>
<div class="text-alignment">
<div
2025-06-09 10:25:54 +08:00
class="align-btn"
:class="{ active: textAlign === 'left' }"
@click="setTextAlign('left')"
>
<svg-icon name="CFleft" size="20" />
</div>
<div
2025-06-09 10:25:54 +08:00
class="align-btn"
:class="{ active: textAlign === 'center' }"
@click="setTextAlign('center')"
>
<svg-icon name="CFcenter" size="20" />
</div>
<div
2025-06-09 10:25:54 +08:00
class="align-btn"
:class="{ active: textAlign === 'right' }"
@click="setTextAlign('right')"
>
<svg-icon name="CFright" size="20" />
</div>
<div
2025-06-09 10:25:54 +08:00
class="align-btn"
:class="{ active: textAlign === 'justify' }"
@click="setTextAlign('justify')"
>
<svg-icon name="CFjustify" size="26" />
</div>
2025-06-09 10:25:54 +08:00
</div>
<div class="text-styles">
<div
2025-06-09 10:25:54 +08:00
class="style-btn"
:class="{ active: underline }"
@click="toggleStyle('underline', !underline)"
>
<div class="style-icon underline-icon">U</div>
</div>
<div
2025-06-09 10:25:54 +08:00
class="style-btn"
:class="{ active: overline }"
@click="toggleStyle('overline', !overline)"
>
<div class="style-icon overline-icon">O</div>
</div>
<div
2025-06-09 10:25:54 +08:00
class="style-btn"
:class="{ active: linethrough }"
@click="toggleStyle('linethrough', !linethrough)"
>
<div class="style-icon linethrough-icon">S</div>
</div>
</div>
<!-- 添加字体色控制区域 -->
<div class="background-controls">
<div class="bg-header">字体色</div>
<div class="bg-options">
<div class="style-btn color-btn" @click="openColorPicker('text')">
<div class="style-icon color-icon" :style="{ backgroundColor: textColor }"></div>
</div>
</div>
2025-06-09 10:25:54 +08:00
</div>
<!-- 添加背景色控制区域 -->
<div class="background-controls">
<div class="bg-header">背景色</div>
<div class="bg-options">
<div class="style-btn color-btn" @click="openColorPicker('background')">
2025-06-09 10:25:54 +08:00
<div
class="style-icon color-icon"
:style="{
backgroundColor: hasTransparentBg ? 'transparent' : backgroundColor,
2025-06-09 10:25:54 +08:00
backgroundImage: hasTransparentBg
? 'linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc)'
: 'none',
backgroundSize: hasTransparentBg ? '8px 8px, 8px 8px' : 'auto',
2025-06-09 10:25:54 +08:00
backgroundPosition: hasTransparentBg ? '0 0, 4px 4px' : '0 0',
}"
></div>
</div>
<div
2025-06-09 10:25:54 +08:00
class="transparent-btn"
:class="{ active: hasTransparentBg }"
@click="setTransparentBackground"
>
透明
</div>
2025-06-09 10:25:54 +08:00
</div>
</div>
</div>
</div>
<!-- 颜色选择器弹窗 -->
<div v-if="showColorPicker" class="color-picker-modal">
<div class="color-picker-container">
<div class="color-picker-header">
<span>{{ colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色" }}</span>
<div class="close-color-picker" @click="closeColorPicker">×</div>
2025-06-09 10:25:54 +08:00
</div>
<div class="color-picker-content">
<input type="color" v-model="currentColor" @change="updateColor" class="color-input" />
2025-06-09 10:25:54 +08:00
<div class="color-presets">
<div
v-for="(color, index) in colorPresets"
:key="index"
class="color-preset"
:style="{ backgroundColor: color }"
@click="selectPresetColor(color)"
></div>
</div>
<div class="confirm-color-btn" @click="confirmColorSelection">确定</div>
2025-06-09 10:25:54 +08:00
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from "vue";
import {
TextContentCommand,
TextFontCommand,
TextSizeCommand,
TextColorCommand,
TextAlignCommand,
TextStyleCommand,
TextSpacingCommand,
TextBackgroundCommand,
TextOpacityCommand,
CompositeTextCommand,
} from "../commands/TextCommands";
export default {
name: "TextEditorPanel",
props: {
canvas: {
type: Object,
required: true,
},
commandManager: {
type: Object,
required: true,
},
},
setup(props) {
// 响应式数据
const visible = ref(false);
const textObject = ref(null);
const layer = ref(null);
// 文本基本属性
const fontFamily = ref("Arial");
const fontSize = ref(24);
const textColor = ref("#000000");
const backgroundColor = ref("#ffffff");
const opacity = ref(1);
const hasTransparentBg = ref(false);
// 文本样式
const fontWeight = ref("normal");
const fontStyle = ref("normal");
const underline = ref(false);
const linethrough = ref(false);
const overline = ref(false);
const textAlign = ref("left");
const charSpacing = ref(0);
const lineHeight = ref(1.16);
// 新增属性
const charSpacingPercent = ref(0);
const textSpacing = ref(0);
// const baseline = ref(0);
2025-06-09 10:25:54 +08:00
const showColorPicker = ref(false);
const colorPickerMode = ref("text"); // 'text' 或 'background'
const currentColor = ref("#000000");
// 颜色预设
const colorPresets = ref([
"#000000",
"#ffffff",
"#ff0000",
"#00ff00",
"#0000ff",
"#ffff00",
"#00ffff",
"#ff00ff",
"#c0c0c0",
"#808080",
"#800000",
"#808000",
"#008000",
"#800080",
"#008080",
]);
// 字体相关
const availableFonts = ref([
{ value: "Symbol", label: "Symbol" },
{ value: "Tamil Sangam MN", label: "Tamil Sangam MN" },
{ value: "Thonburi", label: "Thonburi" },
{ value: "Times New Roman", label: "Times New Roman" },
{ value: "Trebuchet MS", label: "Trebuchet MS" },
{ value: "Verdana", label: "Verdana" },
{ value: "Zapf Dingbats", label: "Zapf Dingbats" },
{ value: "Zapfino", label: "Zapfino" },
{ value: "Arial", label: "Arial" },
{ value: "Helvetica", label: "Helvetica" },
{ value: "Courier New", label: "Courier New" },
{ value: "Georgia", label: "Georgia" },
{ value: "Impact", label: "Impact" },
{ value: "Comic Sans MS", label: "Comic Sans MS" },
{ value: "SimSun", label: "宋体" },
{ value: "SimHei", label: "黑体" },
{ value: "Microsoft YaHei", label: "微软雅黑" },
{ value: "KaiTi", label: "楷体" },
{ value: "FangSong", label: "仿宋" },
]);
// 自定义字体
const customFonts = ref([]);
// 计算属性
const lineHeightToPt = computed(() => {
// 近似转换行高为pt单位
return Math.round(lineHeight.value * fontSize.value);
});
// 方法
const showEditor = (event) => {
const { textObject: eventTextObject, layer: eventLayer } = event.detail;
if (!eventTextObject || !eventLayer) return;
textObject.value = eventTextObject;
layer.value = eventLayer;
// 加载对象的文本属性
loadTextProperties();
// 显示面板
visible.value = true;
};
const close = () => {
visible.value = false;
textObject.value = null;
layer.value = null;
};
const confirmEdit = () => {
// 确认编辑完成
close();
};
const loadTextProperties = () => {
if (!textObject.value) return;
fontFamily.value = textObject.value.fontFamily || "Arial";
fontSize.value = textObject.value.fontSize || 24;
textColor.value = textObject.value.fill || "#000000";
backgroundColor.value = textObject.value.textBackgroundColor || "#ffffff";
hasTransparentBg.value = !textObject.value.textBackgroundColor;
opacity.value = textObject.value.opacity !== undefined ? textObject.value.opacity : 1;
2025-06-09 10:25:54 +08:00
// 样式
fontWeight.value = textObject.value.fontWeight || "normal";
fontStyle.value = textObject.value.fontStyle || "normal";
underline.value = textObject.value.underline || false;
linethrough.value = textObject.value.linethrough || false;
overline.value = textObject.value.overline || false;
textAlign.value = textObject.value.textAlign || "left";
charSpacing.value = textObject.value.charSpacing || 0;
lineHeight.value = textObject.value.lineHeight || 1.16;
// 转换字符间距为百分比显示
charSpacingPercent.value = charSpacing.value / 10;
textSpacing.value = charSpacingPercent.value; // 暂用相同值
// baseline.value = 0; // Fabric.js没有直接支持基线偏移用0初始化
2025-06-09 10:25:54 +08:00
};
const selectFont = (fontName) => {
fontFamily.value = fontName;
updateFont();
};
// 字体更新
const updateFont = () => {
if (!textObject.value || !props.canvas) return;
const command = new TextFontCommand({
canvas: props.canvas,
textObject: textObject.value,
newFont: fontFamily.value,
});
executeCommand(command);
};
// 字号更新
const updateFontSize = () => {
if (!textObject.value || !props.canvas) return;
const command = new TextSizeCommand({
canvas: props.canvas,
textObject: textObject.value,
newSize: fontSize.value,
});
executeCommand(command);
};
// 颜色选择器相关功能
const openColorPicker = (mode) => {
colorPickerMode.value = mode;
currentColor.value = mode === "text" ? textColor.value : backgroundColor.value;
2025-06-09 10:25:54 +08:00
showColorPicker.value = true;
};
const closeColorPicker = () => {
showColorPicker.value = false;
};
const updateColor = () => {
if (colorPickerMode.value === "text") {
textColor.value = currentColor.value;
updateTextColor();
} else {
backgroundColor.value = currentColor.value;
hasTransparentBg.value = false;
updateBackgroundColor();
}
};
const selectPresetColor = (color) => {
currentColor.value = color;
updateColor();
};
const confirmColorSelection = () => {
updateColor();
closeColorPicker();
};
// 文本颜色更新
const updateTextColor = () => {
if (!textObject.value || !props.canvas) return;
const command = new TextColorCommand({
canvas: props.canvas,
textObject: textObject.value,
newColor: textColor.value,
});
executeCommand(command);
};
// 背景颜色更新
const updateBackgroundColor = () => {
if (!textObject.value || !props.canvas) return;
const command = new TextBackgroundCommand({
canvas: props.canvas,
textObject: textObject.value,
newColor: hasTransparentBg.value ? "" : backgroundColor.value,
});
executeCommand(command);
};
// 设置透明背景
const setTransparentBackground = () => {
hasTransparentBg.value = !hasTransparentBg.value;
updateBackgroundColor();
};
// 对齐方式更新
const setTextAlign = (align) => {
if (!textObject.value || !props.canvas) return;
textAlign.value = align;
const command = new TextAlignCommand({
canvas: props.canvas,
textObject: textObject.value,
newAlign: align,
});
executeCommand(command);
};
// 样式切换
const toggleStyle = (property, value) => {
if (!textObject.value || !props.canvas) return;
// 动态更新相应的ref
switch (property) {
case "underline":
underline.value = value;
break;
case "linethrough":
linethrough.value = value;
break;
case "overline":
overline.value = value;
break;
}
const command = new TextStyleCommand({
canvas: props.canvas,
textObject: textObject.value,
property: property,
newValue: value,
});
executeCommand(command);
};
// 字符间距更新
const updateCharSpacing = () => {
if (!textObject.value || !props.canvas) return;
// 将百分比转换为Fabric.js使用的字符间距值
charSpacing.value = charSpacingPercent.value * 10;
const command = new TextSpacingCommand({
canvas: props.canvas,
textObject: textObject.value,
property: "charSpacing",
newValue: charSpacing.value,
});
executeCommand(command);
};
// 文本间距更新 (在Fabric.js中实际上也是控制charSpacing)
const updateTextSpacing = () => {
if (!textObject.value || !props.canvas) return;
// 这里用textSpacing更新charSpacingPercent保持两个滑块同步
charSpacingPercent.value = textSpacing.value;
updateCharSpacing();
};
// 行高更新
const updateLineHeight = () => {
if (!textObject.value || !props.canvas) return;
const command = new TextSpacingCommand({
canvas: props.canvas,
textObject: textObject.value,
property: "lineHeight",
newValue: lineHeight.value,
});
executeCommand(command);
};
// // 基线更新 (Fabric.js没有直接支持可能需要自定义实现)
// const updateBaseline = () => {
// // console.log("基线调整功能待实现", baseline.value);
// // 注意Fabric.js 5没有直接支持基线调整
// // 可能需要通过自定义处理或CSS方式实现
// };
2025-06-09 10:25:54 +08:00
// 透明度更新
const updateOpacity = () => {
if (!textObject.value || !props.canvas) return;
const command = new TextOpacityCommand({
canvas: props.canvas,
textObject: textObject.value,
newOpacity: opacity.value,
});
executeCommand(command);
};
// 执行命令
const executeCommand = (command) => {
if (props.commandManager) {
props.commandManager.execute(command);
} else {
command.execute();
}
};
// 生命周期钩子
onMounted(() => {
// 监听显示文本编辑面板事件
document.addEventListener("showTextEditor", showEditor);
});
onUnmounted(() => {
document.removeEventListener("showTextEditor", showEditor);
});
// 返回所有需要在模板中使用的数据和方法
return {
visible,
textObject,
layer,
fontFamily,
fontSize,
textColor,
backgroundColor,
opacity,
hasTransparentBg,
fontWeight,
fontStyle,
underline,
linethrough,
overline,
textAlign,
charSpacing,
lineHeight,
charSpacingPercent,
textSpacing,
// baseline,
2025-06-09 10:25:54 +08:00
showColorPicker,
colorPickerMode,
currentColor,
colorPresets,
availableFonts,
customFonts,
lineHeightToPt,
showEditor,
close,
confirmEdit,
loadTextProperties,
selectFont,
updateFont,
updateFontSize,
openColorPicker,
closeColorPicker,
updateColor,
selectPresetColor,
confirmColorSelection,
updateTextColor,
updateBackgroundColor,
setTransparentBackground,
setTextAlign,
toggleStyle,
updateCharSpacing,
updateTextSpacing,
updateLineHeight,
// updateBaseline,
2025-06-09 10:25:54 +08:00
updateOpacity,
executeCommand,
};
},
};
</script>
<style scoped>
.text-editor-panel {
position: absolute;
bottom: 22px;
left: 50%;
width: 90%;
max-width: 886px;
min-width: 300px;
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-radius: 12px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
z-index: 1000;
transform: translateX(-50%);
color: #333; /* 文字颜色改为深色 */
border-top: 1px solid rgba(0, 0, 0, 0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.text-editor-panel.is-active {
/* transform: translateY(0); */
}
.text-editor-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background-color: rgba(255, 255, 255, 0.8);
}
.header-btn {
background: none;
border: none;
color: #333; /* 按钮文字颜色改为深色 */
font-size: 14px;
cursor: pointer;
padding: 5px 10px;
}
.header-actions {
display: flex;
gap: 15px;
}
.cancel-btn {
color: #666; /* 取消按钮颜色 */
}
.confirm-btn {
color: #4285f4; /* 确认按钮颜色改为蓝色 */
font-weight: 500;
}
.text-editor-content {
display: flex;
height: 290px;
background-color: #fff;
}
.edit-column {
padding: 10px;
border-right: 1px solid rgba(0, 0, 0, 0.05);
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.font-column {
width: 20%;
overflow: hidden;
min-width: 180px;
}
.style-column {
width: 20%;
min-width: 160px;
}
.design-column {
width: 35%;
}
.props-column {
width: 25%;
border-right: none;
min-width: 220px;
}
.column-header {
font-size: 14px;
color: #333;
margin-bottom: 6px;
font-weight: 500;
}
/* 字体列表样式 */
.font-list {
max-height: 240px;
overflow-y: auto;
}
.font-item {
padding: 8px 5px;
cursor: pointer;
font-size: 14px;
border-radius: 4px;
color: #333;
}
.font-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.font-item.active {
background-color: rgba(66, 133, 244, 0.1);
color: #4285f4;
}
/* 样式预览 */
.style-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80%;
}
.style-name {
font-size: 14px;
margin-bottom: 20px;
color: #333;
}
.style-sample {
font-size: 32px;
color: #333;
font-style: italic;
}
/* 设计参数样式 */
.param-item {
margin-bottom: 15px;
}
.param-label {
font-size: 12px;
margin-bottom: 8px;
color: #666;
}
.param-control {
display: flex;
align-items: center;
gap: 10px;
}
.slider-control {
flex: 1;
height: 4px;
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
-webkit-appearance: none;
appearance: none;
}
.slider-control::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #4285f4;
cursor: pointer;
}
.param-value {
font-size: 12px;
width: 60px;
text-align: right;
color: #333;
}
/* 字体属性样式 */
.text-alignment,
.text-styles {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: space-around;
}
.align-btn,
.style-btn {
width: 40px;
height: 40px;
background-color: rgba(0, 0, 0, 0.05);
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.align-btn:hover,
.style-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.align-btn.active,
.style-btn.active {
background-color: rgba(66, 133, 244, 0.1);
color: #4285f4;
}
.align-icon,
.style-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.align-btn.active .align-icon,
.style-btn.active .style-icon {
color: #4285f4;
}
.align-left:before {
content: "≡";
}
.align-center:before {
content: "≡";
}
.align-right:before {
content: "≡";
}
.align-justify:before {
content: "≡";
}
.underline-icon {
text-decoration: underline;
}
.overline-icon {
text-decoration: overline;
}
.linethrough-icon {
text-decoration: line-through;
}
.color-icon {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.1);
flex: none;
}
/* 背景色控制区域 */
.background-controls {
margin-top: 20px;
}
.bg-header {
font-size: 14px;
margin-bottom: 10px;
color: #333;
text-align: left;
2025-06-09 10:25:54 +08:00
}
.bg-options {
display: flex;
align-items: center;
gap: 10px;
}
.transparent-btn {
padding: 6px 12px;
font-size: 12px;
background-color: rgba(0, 0, 0, 0.05);
color: #333;
border: none;
border-radius: 4px;
cursor: pointer;
}
.transparent-btn.active {
background-color: rgba(66, 133, 244, 0.1);
color: #4285f4;
}
/* 颜色选择器弹窗 */
.color-picker-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.color-picker-container {
background-color: #ffffff;
border-radius: 12px;
width: 280px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.color-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
color: #333;
}
.close-color-picker {
background: none;
border: none;
color: #666;
font-size: 18px;
cursor: pointer;
padding: 0;
}
.color-picker-content {
padding: 20px;
}
.color-input {
width: 100%;
height: 40px;
border: none;
margin-bottom: 15px;
cursor: pointer;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.color-presets {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
margin-bottom: 15px;
}
.color-preset {
width: 100%;
aspect-ratio: 1/1;
border-radius: 4px;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.confirm-color-btn {
width: 100%;
padding: 8px;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.confirm-color-btn:hover {
background-color: #3b77db;
}
</style>