Files
aida_front/src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue
李志鹏 7bc82af120 fix
2025-11-11 14:28:41 +08:00

1099 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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,active:!closePanel }">
<div class="btn" @click="setClosePanel"><i class="fi fi-br-angle-left"></i></div>
<div class="text-editor-panel-header">
<div class="header-btn import-btn">{{ $t('Canvas.EditTextStyle') }}</div>
<div class="header-actions">
<div class="header-btn cancel-btn" @click="close">{{ $t('Canvas.close') }}</div>
<div class="header-btn confirm-btn" @click="confirmEdit">{{ $t('Canvas.confirmEdit') }}</div>
</div>
</div>
<div class="text-editor-content">
<!-- 字体选择列表 -->
<div class="edit-column font-column">
<div class="column-header">{{ $t('Canvas.fontFamily') }}</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">{{ $t('Canvas.fontStyle') }}</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">{{ $t('Canvas.design') }}</div>
<div class="param-item">
<div class="param-label">{{ $t('Canvas.size') }}</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">{{ $t('Canvas.charSpacing') }}</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">{{ $t('Canvas.lineHeight') }}</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">
<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> -->
<div class="param-item">
<div class="param-label">{{ $t('Canvas.opacity') }}</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">{{ $t('Canvas.property') }}</div>
<div class="text-alignment">
<div
class="align-btn"
:class="{ active: textAlign === 'left' }"
@click="setTextAlign('left')"
>
<svg-icon name="CFleft" size="20" />
</div>
<div
class="align-btn"
:class="{ active: textAlign === 'center' }"
@click="setTextAlign('center')"
>
<svg-icon name="CFcenter" size="20" />
</div>
<div
class="align-btn"
:class="{ active: textAlign === 'right' }"
@click="setTextAlign('right')"
>
<svg-icon name="CFright" size="20" />
</div>
<div
class="align-btn"
:class="{ active: textAlign === 'justify' }"
@click="setTextAlign('justify')"
>
<svg-icon name="CFjustify" size="26" />
</div>
</div>
<div class="text-styles">
<div
class="style-btn"
:class="{ active: underline }"
@click="toggleStyle('underline', !underline)"
>
<div class="style-icon underline-icon">U</div>
</div>
<div
class="style-btn"
:class="{ active: overline }"
@click="toggleStyle('overline', !overline)"
>
<div class="style-icon overline-icon">O</div>
</div>
<div
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">{{ $t('Canvas.fontColor') }}</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>
</div>
<!-- 添加背景色控制区域 -->
<div class="background-controls">
<div class="bg-header">{{ $t('Canvas.BGColor') }}</div>
<div class="bg-options">
<div class="style-btn color-btn" @click="openColorPicker('background')">
<div
class="style-icon color-icon"
:style="{
backgroundColor: hasTransparentBg ? 'transparent' : backgroundColor,
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',
backgroundPosition: hasTransparentBg ? '0 0, 4px 4px' : '0 0',
}"
></div>
</div>
<div
class="transparent-btn"
:class="{ active: hasTransparentBg }"
@click="setTransparentBackground"
>
{{ $t('Canvas.BGOpacity') }}
</div>
</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" ? $t('Canvas.SelectTextColor') : $t('Canvas.SelectBGColor') }}</span>
<div class="close-color-picker" @click="closeColorPicker">×</div>
</div>
<div class="color-picker-content">
<input type="color" v-model="currentColor" @change="updateColor" class="color-input" />
<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">{{ $t('Canvas.ok') }}</div>
</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);
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 closePanel = ref(false)
const setClosePanel = ()=>{
closePanel.value = !closePanel.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;
closePanel.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;
// 样式
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初始化
};
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;
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方式实现
// };
// 透明度更新
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,
setClosePanel,
closePanel,
hasTransparentBg,
fontWeight,
fontStyle,
underline,
linethrough,
overline,
textAlign,
charSpacing,
lineHeight,
charSpacingPercent,
textSpacing,
// baseline,
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,
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;
&.active{
transform: translateY(100%);
> .btn{
> i{
transform: rotate(90deg);
}
}
}
> .btn{
width: 100%;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 22px;
> i{
font-size: 1.4rem;
display: block;
transform: rotate(270deg);
}
}
}
.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;
}
.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>