2025-06-09 10:25:54 +08:00
|
|
|
|
<!-- filepath: /Users/aaron/work/pc/air/canvasEdit/src/components/CanvasEditor/components/TextEditorPanel.vue -->
|
|
|
|
|
|
<template>
|
2025-07-14 01:00:23 +08:00
|
|
|
|
<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">
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<div class="header-btn import-btn">编辑文本样式</div>
|
2025-06-09 10:25:54 +08:00
|
|
|
|
<div class="header-actions">
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<!-- <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>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</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">
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<div
|
2025-06-09 10:25:54 +08:00
|
|
|
|
class="align-btn"
|
|
|
|
|
|
:class="{ active: textAlign === 'left' }"
|
|
|
|
|
|
@click="setTextAlign('left')"
|
|
|
|
|
|
>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<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')"
|
|
|
|
|
|
>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<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')"
|
|
|
|
|
|
>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<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')"
|
|
|
|
|
|
>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<svg-icon name="CFjustify" size="26" />
|
|
|
|
|
|
</div>
|
2025-06-09 10:25:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="text-styles">
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<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>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</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>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</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>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 添加字体色控制区域 -->
|
|
|
|
|
|
<div class="background-controls">
|
|
|
|
|
|
<div class="bg-header">字体色</div>
|
|
|
|
|
|
<div class="bg-options">
|
|
|
|
|
|
<div class="style-btn color-btn" @click="openColorPicker('text')">
|
2025-07-14 01:00:23 +08:00
|
|
|
|
<div class="style-icon color-icon" :style="{ backgroundColor: textColor }"></div>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-06-09 10:25:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 添加背景色控制区域 -->
|
|
|
|
|
|
<div class="background-controls">
|
|
|
|
|
|
<div class="bg-header">背景色</div>
|
|
|
|
|
|
<div class="bg-options">
|
2025-07-14 01:00:23 +08:00
|
|
|
|
<div class="style-btn color-btn" @click="openColorPicker('background')">
|
2025-06-09 10:25:54 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="style-icon color-icon"
|
|
|
|
|
|
:style="{
|
2025-07-14 01:00:23 +08:00
|
|
|
|
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',
|
2025-07-14 01:00:23 +08:00
|
|
|
|
backgroundSize: hasTransparentBg ? '8px 8px, 8px 8px' : 'auto',
|
2025-06-09 10:25:54 +08:00
|
|
|
|
backgroundPosition: hasTransparentBg ? '0 0, 4px 4px' : '0 0',
|
|
|
|
|
|
}"
|
|
|
|
|
|
></div>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
2025-06-09 10:25:54 +08:00
|
|
|
|
class="transparent-btn"
|
|
|
|
|
|
:class="{ active: hasTransparentBg }"
|
|
|
|
|
|
@click="setTransparentBackground"
|
|
|
|
|
|
>
|
|
|
|
|
|
透明
|
2025-06-26 23:26:50 +08:00
|
|
|
|
</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">
|
2025-07-14 01:00:23 +08:00
|
|
|
|
<span>{{ colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色" }}</span>
|
2025-06-26 23:26:50 +08:00
|
|
|
|
<div class="close-color-picker" @click="closeColorPicker">×</div>
|
2025-06-09 10:25:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="color-picker-content">
|
2025-07-14 01:00:23 +08:00
|
|
|
|
<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>
|
2025-07-14 01:00:23 +08:00
|
|
|
|
<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);
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// 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;
|
2025-07-14 01:00:23 +08:00
|
|
|
|
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; // 暂用相同值
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// 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;
|
2025-07-14 01:00:23 +08:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// // 基线更新 (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,
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// 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,
|
2025-07-14 01:00:23 +08:00
|
|
|
|
// 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;
|
2025-06-26 23:26:50 +08:00
|
|
|
|
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>
|