1109 lines
27 KiB
Vue
1109 lines
27 KiB
Vue
<!-- 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);
|
||
document.addEventListener("hideTextEditor", close);
|
||
if(props.canvas) {
|
||
// props.canvas.on("text:editing:entered", showEditor);
|
||
props.canvas.on("text:editing:exited", close);
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
document.removeEventListener("showTextEditor", showEditor);
|
||
document.removeEventListener("hideTextEditor", close);
|
||
if(props.canvas) {
|
||
// props.canvas.off("text:editing:entered", showEditor);
|
||
props.canvas.off("text:editing:exited", close);
|
||
}
|
||
});
|
||
|
||
// 返回所有需要在模板中使用的数据和方法
|
||
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>
|