jvs-ppt-ui 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/lib/check_imgs.html +18 -0
  2. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.css +187 -27
  3. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.js +1 -1
  4. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.json +345 -65
  5. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.ttf +0 -0
  6. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff +0 -0
  7. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff2 +0 -0
  8. package/dist/lib/jvs-ppt-ui/img/al.png +0 -0
  9. package/dist/lib/jvs-ppt-ui/img/d.png +0 -0
  10. package/dist/lib/jvs-ppt-ui/img/dl.png +0 -0
  11. package/dist/lib/jvs-ppt-ui/img/l.png +0 -0
  12. package/dist/lib/jvs-ppt-ui.es.js +44973 -43349
  13. package/dist/lib/jvs-ppt-ui.umd.js +102 -102
  14. package/dist/lib/style.css +1 -1
  15. package/package.json +1 -1
  16. package/scripts/check-icons.mjs +22 -0
  17. package/scripts/patch-anim-icons.mjs +45 -0
  18. package/src/assets/iconfont/iconfont.css +187 -27
  19. package/src/assets/iconfont/iconfont.js +1 -1
  20. package/src/assets/iconfont/iconfont.json +345 -65
  21. package/src/assets/iconfont/iconfont.ttf +0 -0
  22. package/src/assets/iconfont/iconfont.woff +0 -0
  23. package/src/assets/iconfont/iconfont.woff2 +0 -0
  24. package/src/components/CanvasArea.vue +218 -193
  25. package/src/components/ChartTypeSelector.vue +509 -148
  26. package/src/components/Header.vue +327 -44
  27. package/src/components/PresentationGroupChild.vue +44 -13
  28. package/src/components/PresentationMode.vue +84 -33
  29. package/src/components/QuickBorderPopover.vue +51 -11
  30. package/src/components/QuickColorPopover.vue +65 -21
  31. package/src/components/SidebarLeft.vue +286 -107
  32. package/src/components/SidebarRight.vue +45 -38
  33. package/src/components/TemplateGallery.vue +1110 -1237
  34. package/src/components/ToastNotification.vue +2 -2
  35. package/src/components/Toolbar.vue +825 -733
  36. package/src/components/canvas/CanvasQuickToolbar.vue +17 -30
  37. package/src/components/canvas/elements/CanvasGroupElement.vue +6 -0
  38. package/src/components/canvas/elements/CanvasShapeElement.vue +198 -38
  39. package/src/components/canvas/elements/CanvasTextElement.vue +4 -3
  40. package/src/components/sidebar/common/SidebarAlignGroup.vue +104 -50
  41. package/src/components/sidebar/common/SidebarBorderAccordion.vue +375 -148
  42. package/src/components/sidebar/common/SidebarPositionGroup.vue +358 -149
  43. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +429 -203
  44. package/src/components/sidebar/common/SidebarShadowAccordion.vue +844 -416
  45. package/src/components/sidebar/panels/GradientEditor.vue +671 -316
  46. package/src/components/sidebar/panels/LayoutThumbSvg.vue +33 -16
  47. package/src/components/sidebar/panels/SidebarAnimationPanel.vue +701 -138
  48. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  49. package/src/components/sidebar/panels/SidebarDesignPanel.vue +1754 -416
  50. package/src/components/sidebar/panels/SidebarMediaFormatPanel.vue +12 -0
  51. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +813 -174
  52. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  53. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1938 -1427
  54. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +791 -210
  55. package/src/composables/canvas/useCanvasShortcuts.ts +20 -6
  56. package/src/composables/canvas/useCanvasText.ts +1 -1
  57. package/src/composables/canvas/useCanvasTransform.ts +44 -0
  58. package/src/composables/presetThemes.ts +43 -44
  59. package/src/composables/sidebar/useSidebarAnimation.ts +101 -25
  60. package/src/composables/sidebar/useSidebarDesign.ts +30 -11
  61. package/src/composables/sidebar/useSidebarElementTransform.ts +10 -1
  62. package/src/composables/sidebar/useSidebarImage.ts +10 -2
  63. package/src/composables/sidebar/useSidebarShapeFormat.ts +562 -107
  64. package/src/composables/useSlides.ts +331 -153
  65. package/src/main.ts +2 -0
  66. package/src/style.css +926 -359
  67. package/src/utils/canvas/elementStyles.ts +42 -1
@@ -1,1427 +1,1938 @@
1
- <script setup lang="ts">
2
- import { ref, computed } from 'vue'
3
- import { getFontVariants } from '../../../configs/fonts'
4
- import QuickColorPopover from '../../QuickColorPopover.vue'
5
- import { type SlideTextType, TEXT_TYPE_LIST } from '../../../utils/textTypes'
6
-
7
- import ulDisc from '../../../assets/list/ul-disc.png'
8
- import ulDiamond from '../../../assets/list/ul-diamond.png'
9
- import ulTriangle from '../../../assets/list/ul-triangle.png'
10
- import ulCheckmark from '../../../assets/list/ul-checkmark.png'
11
- import ulSquare from '../../../assets/list/ul-square.png'
12
- import ulCircle from '../../../assets/list/ul-circle.png'
13
- import olDicimal from '../../../assets/list/ol-dicimal.png'
14
- import olCircledNumbers from '../../../assets/list/ol-circled-numbers.png'
15
- import olLowerAlpha from '../../../assets/list/ol-lower-alpha.png'
16
- import olUpperAlpha from '../../../assets/list/ol-upper-alpha.png'
17
- import olUpperRoman from '../../../assets/list/ol-upper-roman.png'
18
- import olChinese from '../../../assets/list/ol-chinese.png'
19
-
20
- const props = withDefaults(defineProps<{
21
- selectedElement: any
22
- selectedElementIds?: string[]
23
- currentTextType?: SlideTextType
24
- sysFonts?: Array<{ label: string; value: string }>
25
- themeRecipe?: any
26
- fontStyleSupport?: { bold: boolean; italic: boolean; boldItalic: boolean }
27
- styleFontFamily?: string
28
- styleFontSize?: string
29
- styleFontWeightStyle?: string
30
- lineHeightNumber?: number
31
- letterSpacingNumber?: number
32
- styleTextColor?: string
33
- styleTextBackground?: string
34
- styleTextAlign?: string
35
- styleVerticalAlign?: string
36
- styleWritingMode?: string
37
- styleAutoFitType?: string
38
- styleWordWrap?: boolean
39
- paddingTop?: number
40
- paddingRight?: number
41
- paddingBottom?: number
42
- paddingLeft?: number
43
- isPaddingLocked?: boolean
44
- hasUnorderedList?: boolean
45
- hasOrderedList?: boolean
46
- isActiveListStyle?: (type: any, val: string) => boolean
47
- enableTextStroke?: boolean
48
- textStrokeColor?: string
49
- textStrokeWidth?: number
50
- textStrokeStyle?: string
51
- textStrokeAlpha?: number
52
- enableTextShadow?: boolean
53
- textShadowHex?: string
54
- textShadowAlpha?: number
55
- textShadowAngle?: number
56
- textShadowSize?: number
57
- textShadowDistance?: number
58
- textShadowBlur?: number
59
- enableTextReflection?: boolean
60
- textReflectionDistance?: number
61
- textReflectionSize?: number
62
- textReflectionOpacity?: number
63
- textReflectionBlur?: number
64
- recentColors?: string[]
65
- getStyleVal?: (prop: string, fallback?: any) => any
66
- }>(), {
67
- selectedElementIds: () => [],
68
- currentTextType: 'normalText',
69
- sysFonts: () => [],
70
- fontStyleSupport: () => ({ bold: true, italic: true, boldItalic: true }),
71
- styleFontFamily: '默认字体',
72
- styleFontSize: '16px',
73
- styleFontWeightStyle: 'Regular',
74
- lineHeightNumber: 1.3,
75
- letterSpacingNumber: 0,
76
- styleTextColor: '#000000',
77
- styleTextBackground: 'transparent',
78
- styleTextAlign: 'center',
79
- styleVerticalAlign: 'middle',
80
- styleWritingMode: 'horizontal-tb',
81
- styleAutoFitType: 'shape',
82
- styleWordWrap: true,
83
- paddingTop: 4.8,
84
- paddingRight: 9.6,
85
- paddingBottom: 4.8,
86
- paddingLeft: 9.6,
87
- isPaddingLocked: false,
88
- hasUnorderedList: false,
89
- hasOrderedList: false,
90
- enableTextStroke: false,
91
- textStrokeColor: '#000000',
92
- textStrokeWidth: 1,
93
- textStrokeStyle: 'solid',
94
- textStrokeAlpha: 100,
95
- enableTextShadow: false,
96
- textShadowHex: '#000000',
97
- textShadowAlpha: 50,
98
- textShadowAngle: 45,
99
- textShadowSize: 100,
100
- textShadowDistance: 3,
101
- textShadowBlur: 4,
102
- enableTextReflection: false,
103
- textReflectionDistance: 0,
104
- textReflectionSize: 100,
105
- textReflectionOpacity: 10,
106
- textReflectionBlur: 0,
107
- recentColors: () => [],
108
- getStyleVal: () => undefined
109
- })
110
-
111
- const emit = defineEmits<{
112
- (e: 'changeTextType', val: SlideTextType): void
113
- (e: 'changeFontFamily', val: string): void
114
- (e: 'changeFontSize', val: string): void
115
- (e: 'update:styleFontWeightStyle', val: string): void
116
- (e: 'update:lineHeightNumber', val: number): void
117
- (e: 'update:letterSpacingNumber', val: number): void
118
- (e: 'update:styleTextColor', val: string): void
119
- (e: 'update:styleTextBackground', val: string): void
120
- (e: 'update:styleTextAlign', val: string): void
121
- (e: 'update:styleVerticalAlign', val: string): void
122
- (e: 'update:styleWritingMode', val: string): void
123
- (e: 'update:styleAutoFitType', val: string): void
124
- (e: 'update:styleWordWrap', val: boolean): void
125
- (e: 'update:paddingTop', val: number): void
126
- (e: 'update:paddingRight', val: number): void
127
- (e: 'update:paddingBottom', val: number): void
128
- (e: 'update:paddingLeft', val: number): void
129
- (e: 'update:isPaddingLocked', val: boolean): void
130
- (e: 'toggleListStyle', type: string, val: string, isFromPanel?: boolean): void
131
- (e: 'update:enableTextStroke', val: boolean): void
132
- (e: 'update:textStrokeColor', val: string): void
133
- (e: 'update:textStrokeWidth', val: number): void
134
- (e: 'update:textStrokeStyle', val: string): void
135
- (e: 'update:textStrokeAlpha', val: number): void
136
- (e: 'update:enableTextShadow', val: boolean): void
137
- (e: 'update:textShadowHex', val: string): void
138
- (e: 'update:textShadowAlpha', val: number): void
139
- (e: 'update:textShadowAngle', val: number): void
140
- (e: 'update:textShadowSize', val: number): void
141
- (e: 'update:textShadowBlur', val: number): void
142
- (e: 'update:textShadowDistance', val: number): void
143
- (e: 'update:enableTextReflection', val: boolean): void
144
- (e: 'update:textReflectionDistance', val: number): void
145
- (e: 'update:textReflectionSize', val: number): void
146
- (e: 'update:textReflectionOpacity', val: number): void
147
- (e: 'update:textReflectionBlur', val: number): void
148
- (e: 'toggleBold'): void
149
- (e: 'toggleItalic'): void
150
- (e: 'toggleUnderline'): void
151
- (e: 'toggleStrikethrough'): void
152
- }>()
153
-
154
- // Default all accordion sections to true so headers display down-arrow ▼ matching target design
155
- const isListExpanded = ref(true)
156
- const isPaddingExpanded = ref(true)
157
- const isTextBorderExpanded = ref(true)
158
- const isTextShadowExpanded = ref(true)
159
- const isTextReflectionExpanded = ref(true)
160
-
161
- const isUlVisible = ref(false)
162
- const isOlVisible = ref(false)
163
- const textColorPopoverVisible = ref(false)
164
- const textBgPopoverVisible = ref(false)
165
- const textStrokePopoverVisible = ref(false)
166
- const textShadowPopoverVisible = ref(false)
167
-
168
- const fontSizeClean = computed(() => {
169
- return String(props.styleFontSize || '16').replace('px', '')
170
- })
171
-
172
- function checkStyleVal(prop: string, fallback?: any): any {
173
- if (props.getStyleVal) {
174
- const val = props.getStyleVal(prop, fallback)
175
- if (val !== undefined) return val
176
- }
177
- return fallback
178
- }
179
-
180
- const isBoldActive = computed(() => {
181
- return checkStyleVal('fontWeight') === 'bold'
182
- })
183
-
184
- const isItalicActive = computed(() => {
185
- return checkStyleVal('fontStyle') === 'italic'
186
- })
187
-
188
- const isUnderlineActive = computed(() => {
189
- return (checkStyleVal('textDecoration') || '').includes('underline')
190
- })
191
-
192
- const isStrikethroughActive = computed(() => {
193
- return (checkStyleVal('textDecoration') || '').includes('line-through')
194
- })
195
-
196
- const currentTextAlign = computed(() => {
197
- return props.styleTextAlign || checkStyleVal('textAlign', 'center') || 'center'
198
- })
199
-
200
- const currentFontVariants = computed(() => {
201
- return getFontVariants(props.styleFontFamily)
202
- })
203
-
204
- const displaySysFonts = computed(() => {
205
- return props.sysFonts || []
206
- })
207
-
208
- function toggleTextBorderHeader() {
209
- if (!props.enableTextStroke) return
210
- isTextBorderExpanded.value = !isTextBorderExpanded.value
211
- }
212
-
213
- function toggleTextShadowHeader() {
214
- if (!props.enableTextShadow) return
215
- isTextShadowExpanded.value = !isTextShadowExpanded.value
216
- }
217
-
218
- function toggleTextReflectionHeader() {
219
- if (!props.enableTextReflection) return
220
- isTextReflectionExpanded.value = !isTextReflectionExpanded.value
221
- }
222
-
223
- function startDragAngle(e: MouseEvent) {
224
- const dial = e.currentTarget as HTMLElement
225
- const rect = dial.getBoundingClientRect()
226
- const centerX = rect.left + rect.width / 2
227
- const centerY = rect.top + rect.height / 2
228
-
229
- function updateAngle(moveEvent: MouseEvent) {
230
- const dx = moveEvent.clientX - centerX
231
- const dy = moveEvent.clientY - centerY
232
- let angle = Math.round(Math.atan2(dy, dx) * 180 / Math.PI)
233
- if (angle < 0) angle += 360
234
- emit('update:textShadowAngle', angle)
235
- }
236
-
237
- updateAngle(e)
238
-
239
- function stopDrag() {
240
- window.removeEventListener('mousemove', updateAngle)
241
- window.removeEventListener('mouseup', stopDrag)
242
- }
243
-
244
- window.addEventListener('mousemove', updateAngle)
245
- window.addEventListener('mouseup', stopDrag)
246
- }
247
- </script>
248
-
249
- <template>
250
- <div class="sidebar-text-format-panel" style="padding: 16px 0 40px; display: flex; flex-direction: column;">
251
- <div style="padding: 0 16px; display: flex; flex-direction: column; gap: 12px;">
252
- <!-- Text Classification Selector (文本类型分类: title, headline, subheadline, normal text, small text) -->
253
- <div class="shape-property-row">
254
- <span class="shape-property-label">类型</span>
255
- <el-select :model-value="currentTextType || 'normalText'" @change="emit('changeTextType', $event)" size="small"
256
- class="custom-select" style="flex: 1;" placement="bottom-start"
257
- popper-class="sidebar-text-type-popper">
258
- <el-option v-for="item in TEXT_TYPE_LIST" :key="item.type" :value="item.type" :label="`${item.label} (${item.enLabel})`">
259
- <div style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px;">
260
- <div style="display: flex; align-items: center; gap: 6px;">
261
- <span :style="{ color: item.badgeColor, backgroundColor: item.badgeBg, fontSize: '10px', padding: '1px 5px', borderRadius: '3px', fontWeight: 600, minWidth: '40px', textAlign: 'center' }">{{ item.tag }}</span>
262
- <span style="font-size: 13px; font-weight: 500;">{{ item.label }}</span>
263
- </div>
264
- <span style="font-size: 11px; color: #94a3b8;">{{ item.defaultFontSize }}px</span>
265
- </div>
266
- </el-option>
267
- </el-select>
268
- </div>
269
-
270
- <!-- Shape Text Font Family -->
271
- <div class="shape-property-row">
272
- <span class="shape-property-label">字体</span>
273
- <el-select :model-value="styleFontFamily" @change="emit('changeFontFamily', $event)" size="small"
274
- class="custom-select" style="flex: 1;" filterable placement="bottom-start"
275
- :fallback-placements="['bottom-start', 'bottom', 'top-start']" popper-class="sidebar-font-popper">
276
- <el-option label="默认字体" value="默认字体" />
277
- <el-option v-for="font in displaySysFonts" :key="font.value" :label="font.label" :value="font.value">
278
- <span :style="{ fontFamily: font.value }">{{ font.label }}</span>
279
- </el-option>
280
- </el-select>
281
- </div>
282
-
283
- <!-- Shape Text Style & Size -->
284
- <div class="shape-property-row">
285
- <span class="shape-property-label">样式</span>
286
- <div style="display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 1;">
287
- <!-- Font Weight/Style Select -->
288
- <el-select :model-value="styleFontWeightStyle"
289
- @update:model-value="emit('update:styleFontWeightStyle', $event)" size="small" class="custom-select"
290
- style="flex: 1;">
291
- <el-option v-for="variant in currentFontVariants" :key="variant.value" :label="variant.label"
292
- :value="variant.value" />
293
- </el-select>
294
- <!-- Font Size Select -->
295
- <el-select :model-value="fontSizeClean" @change="emit('changeFontSize', $event)" size="small"
296
- class="custom-select" style="flex: 1;" filterable allow-create default-first-option placeholder="字号">
297
- <el-option v-for="size in ['12', '14', '16', '18', '20', '24', '28', '32', '36', '44', '52', '60', '72']"
298
- :key="size" :label="size" :value="size" />
299
- </el-select>
300
- </div>
301
- </div>
302
-
303
- <!-- Line Height & Letter Spacing -->
304
- <div class="shape-property-row">
305
- <span class="shape-property-label"></span>
306
- <div style="display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 1;">
307
- <!-- Line height -->
308
- <div class="custom-num-input" style="flex: 1; height: 32px;">
309
- <svg class="icon"
310
- style="width: 14px; height: 14px; margin-right: 6px; color: var(--text-secondary, #64748b); flex-shrink: 0;">
311
- <use xlink:href="#icon-hanggao"></use>
312
- </svg>
313
- <input type="number" :value="lineHeightNumber"
314
- @input="emit('update:lineHeightNumber', Number(($event.target as HTMLInputElement).value))"
315
- class="custom-num-field" step="0.1" min="0.5" max="5" />
316
- </div>
317
- <!-- Letter spacing -->
318
- <div class="custom-num-input" style="flex: 1; height: 32px;">
319
- <svg class="icon"
320
- style="width: 14px; height: 14px; margin-right: 6px; color: var(--text-secondary, #64748b); flex-shrink: 0;">
321
- <use xlink:href="#icon-zijianju"></use>
322
- </svg>
323
- <input type="number" :value="letterSpacingNumber"
324
- @input="emit('update:letterSpacingNumber', Number(($event.target as HTMLInputElement).value))"
325
- class="custom-num-field" min="-10" max="50" />
326
- </div>
327
- </div>
328
- </div>
329
-
330
- <!-- Bold, Italic, Underline, Strikethrough Buttons -->
331
- <div class="shape-property-row">
332
- <span class="shape-property-label"></span>
333
- <div style="display: flex; align-items: center; flex: 1;">
334
- <div class="font-action-btn-rows font-styles">
335
- <div class="font-action-btn-clean"
336
- :style="isBoldActive ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
337
- @click="emit('toggleBold')">
338
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
339
- <use :xlink:href="isBoldActive ? '#icon-jiacu-xuanzhong' : '#icon-jiacu'"></use>
340
- </svg>
341
- </div>
342
- <div class="font-action-btn-clean"
343
- :style="isItalicActive ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
344
- @click="emit('toggleItalic')">
345
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
346
- <use :xlink:href="isItalicActive ? '#icon-qingxie-xuanzhong' : '#icon-qingxie'"></use>
347
- </svg>
348
- </div>
349
- <div class="font-action-btn-clean"
350
- :style="isUnderlineActive ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
351
- @click="emit('toggleUnderline')">
352
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
353
- <use :xlink:href="isUnderlineActive ? '#icon-xiahuaxian-xuanzhong' : '#icon-xiahuaxian'"></use>
354
- </svg>
355
- </div>
356
- <div class="font-action-btn-clean"
357
- :style="isStrikethroughActive ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
358
- @click="emit('toggleStrikethrough')">
359
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
360
- <use :xlink:href="isStrikethroughActive ? '#icon-shanchuxian-xuanzhong' : '#icon-shanchuxian'"></use>
361
- </svg>
362
- </div>
363
- </div>
364
- </div>
365
- </div>
366
-
367
- <!-- Alignment Buttons (Row 1: 水平对齐 5个; Row 2: 垂直对齐 3个 换行显示) -->
368
- <div class="shape-property-row">
369
- <span class="shape-property-label"></span>
370
- <div style="display: flex; align-items: center; flex: 1;">
371
- <!-- Horizontal Alignments -->
372
- <div class="font-action-btn-rows" style="flex: 1;">
373
- <div class="font-action-btn-clean"
374
- :style="currentTextAlign === 'left' ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
375
- title="左对齐" @click="emit('update:styleTextAlign', 'left')">
376
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
377
- <use :xlink:href="currentTextAlign === 'left' ? '#icon-zuoduiqi-xuanzhong' : '#icon-zuoduiqi1'"></use>
378
- </svg>
379
- </div>
380
- <div class="font-action-btn-clean"
381
- :style="(currentTextAlign === 'center' || !currentTextAlign) ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
382
- title="居中对齐" @click="emit('update:styleTextAlign', 'center')">
383
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
384
- <use
385
- :xlink:href="(currentTextAlign === 'center' || !currentTextAlign) ? '#icon-juzhongduiqi-xuanzhong' : '#icon-juzhongduiqi'">
386
- </use>
387
- </svg>
388
- </div>
389
- <div class="font-action-btn-clean"
390
- :style="currentTextAlign === 'right' ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
391
- title="右对齐" @click="emit('update:styleTextAlign', 'right')">
392
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
393
- <use :xlink:href="currentTextAlign === 'right' ? '#icon-youduiqi3-copy-copy' : '#icon-youduiqi3-copy'">
394
- </use>
395
- </svg>
396
- </div>
397
- <div class="font-action-btn-clean"
398
- :style="currentTextAlign === 'justify' ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
399
- title="两端对齐" @click="emit('update:styleTextAlign', 'justify')">
400
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
401
- <use xlink:href="#icon-liangduanduiqi"></use>
402
- </svg>
403
- </div>
404
- <div class="font-action-btn-clean"
405
- :style="currentTextAlign === 'distribute' ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
406
- title="分散对齐" @click="emit('update:styleTextAlign', 'distribute')">
407
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
408
- <use xlink:href="#icon-fensanduiqi"></use>
409
- </svg>
410
- </div>
411
- </div>
412
- </div>
413
- </div>
414
-
415
- <!-- Vertical Alignments (换行显示) -->
416
- <div class="shape-property-row">
417
- <span class="shape-property-label"></span>
418
- <div style="display: flex; align-items: center; flex: 1;">
419
- <div class="font-action-btn-rows" style="flex: 1;">
420
- <div class="font-action-btn-clean"
421
- :style="styleVerticalAlign === 'top' ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
422
- title="顶对齐" @click="emit('update:styleVerticalAlign', 'top')">
423
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
424
- <use :xlink:href="styleVerticalAlign === 'top' ? '#icon-dingduiqi-xuanzhong' : '#icon-dingduiqi1'">
425
- </use>
426
- </svg>
427
- </div>
428
- <div class="font-action-btn-clean"
429
- :style="(styleVerticalAlign === 'middle' || !styleVerticalAlign) ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
430
- title="居中对齐" @click="emit('update:styleVerticalAlign', 'middle')">
431
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
432
- <use
433
- :xlink:href="(styleVerticalAlign === 'middle' || !styleVerticalAlign) ? '#icon-chuizhiduiqi-xuanzhong' : '#icon-chuizhiduiqi'">
434
- </use>
435
- </svg>
436
- </div>
437
- <div class="font-action-btn-clean"
438
- :style="styleVerticalAlign === 'bottom' ? { background: '#ffffff', color: 'var(--accent-primary, #6366f1)', boxShadow: '0 1px 3px rgba(0,0,0,0.15)' } : {}"
439
- title="底对齐" @click="emit('update:styleVerticalAlign', 'bottom')">
440
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
441
- <use :xlink:href="styleVerticalAlign === 'bottom' ? '#icon-diduiqi1-copy' : '#icon-diduiqi1'"></use>
442
- </svg>
443
- </div>
444
- </div>
445
- </div>
446
- </div>
447
-
448
- <!-- Shape Text Color -->
449
- <div class="shape-property-row">
450
- <span class="shape-property-label">字体颜色</span>
451
- <div class="color-picker-cell-wrapper">
452
- <QuickColorPopover v-model:visible="textColorPopoverVisible" :color="styleTextColor || '#000000'"
453
- :recent-colors="recentColors" @select-color="emit('update:styleTextColor', $event)">
454
- <template #reference>
455
- <div class="color-picker-pill-wrapper">
456
- <div style="display: flex; align-items: center; gap: 8px;">
457
- <div :style="{ background: styleTextColor || '#000000' }"
458
- style="width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.15);">
459
- </div>
460
- <span
461
- style="font-size: 11px; color: var(--text-primary, #1e293b); font-family: monospace; font-weight: 500;">{{
462
- (styleTextColor || '#000000').replace('#', '').toUpperCase() }}</span>
463
- </div>
464
- </div>
465
- </template>
466
- </QuickColorPopover>
467
- </div>
468
- </div>
469
-
470
- <!-- Text Highlight Color -->
471
- <div class="shape-property-row">
472
- <span class="shape-property-label">高亮色</span>
473
- <div class="color-picker-cell-wrapper">
474
- <QuickColorPopover v-model:visible="textBgPopoverVisible" :color="styleTextBackground || 'transparent'"
475
- :recent-colors="recentColors" @select-color="emit('update:styleTextBackground', $event)">
476
- <template #reference>
477
- <div class="color-picker-pill-wrapper">
478
- <div style="display: flex; align-items: center; gap: 8px;">
479
- <div :style="{ background: styleTextBackground || 'transparent' }"
480
- style="width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.15);">
481
- </div>
482
- <span
483
- style="font-size: 11px; color: var(--text-primary, #1e293b); font-family: monospace; font-weight: 500;">{{
484
- (styleTextBackground || 'transparent').replace('#', '').toUpperCase() }}</span>
485
- </div>
486
- </div>
487
- </template>
488
- </QuickColorPopover>
489
- </div>
490
- </div>
491
-
492
- <!-- Text Direction -->
493
- <div class="shape-property-row">
494
- <span class="shape-property-label">文字方向</span>
495
- <el-select :model-value="styleWritingMode" @update:model-value="emit('update:styleWritingMode', $event)"
496
- size="small" class="custom-select" style="flex: 1;">
497
- <el-option label="横向" value="horizontal-tb" />
498
- <el-option label="竖向(从左到右)" value="vertical-lr" />
499
- <el-option label="竖向(从右到左)" value="vertical-rl" />
500
- </el-select>
501
- </div>
502
-
503
- <!-- Text Auto Fit overflow setting -->
504
- <div class="shape-property-row">
505
- <span class="shape-property-label">自动调整</span>
506
- <el-select :model-value="styleAutoFitType" @update:model-value="emit('update:styleAutoFitType', $event)"
507
- size="small" class="custom-select" style="flex: 1;">
508
- <el-option label="文字溢出不自动调整" value="none" />
509
- <el-option label="溢出时缩减文字大小" value="text" />
510
- <el-option label="根据文字调整大小" value="shape" />
511
- </el-select>
512
- </div>
513
-
514
- <!-- Word wrap setting -->
515
- <div class="shape-property-row">
516
- <span class="shape-property-label">是否自动换行</span>
517
- <el-select :model-value="styleWordWrap" @update:model-value="emit('update:styleWordWrap', $event)" size="small"
518
- class="custom-select" style="flex: 1;">
519
- <el-option label="是" :value="true" />
520
- <el-option label="否" :value="false" />
521
- </el-select>
522
- </div>
523
- </div>
524
-
525
- <!-- Divider above List Accordion -->
526
- <div class="control-divider" style="margin: 12px 0 6px; border-top: 1px solid var(--border-color, #E2E8F0);"></div>
527
-
528
- <!-- List Numbering Accordion -->
529
- <div class="control-group shape-accordion-group">
530
- <div class="shape-accordion-header" @click="isListExpanded = !isListExpanded">
531
- <span class="control-label shape-accordion-header-label">
532
- <span class="accordion-arrow"
533
- :style="{ transform: isListExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>编号</span>
534
- </div>
535
-
536
- <div v-show="isListExpanded" class="shape-accordion-content" style="padding-top: 4px;">
537
- <!-- Selector Buttons Row with Popovers -->
538
- <div style="display: flex; gap: 8px;">
539
- <!-- Unordered List Split Button -->
540
- <div class="list-split-btn-group" :style="{
541
- display: 'flex',
542
- alignItems: 'center',
543
- height: '32px',
544
- background: hasUnorderedList ? '#e0ebff' : '#F5F6F7',
545
- borderRadius: '6px',
546
- color: hasUnorderedList ? '#2563eb' : '#475569',
547
- fontSize: '12px',
548
- fontWeight: '500',
549
- flex: 1,
550
- overflow: 'hidden',
551
- border: '1px solid transparent'
552
- }">
553
- <!-- Left part: toggle list -->
554
- <div class="list-split-left-action" @click="emit('toggleListStyle', 'ul', 'disc')" @mousedown.prevent style="
555
- display: flex;
556
- align-items: center;
557
- gap: 6px;
558
- padding: 0 8px;
559
- flex: 1;
560
- height: 100%;
561
- cursor: pointer;
562
- user-select: none;
563
- ">
564
- <svg class="icon" style="width: 14px; height: 14px; fill: currentColor;">
565
- <use xlink:href="#icon-wuxuliebiao"></use>
566
- </svg>
567
- <span>无序列表</span>
568
- </div>
569
-
570
- <!-- Vertical separator line -->
571
- <div style="width: 1px; height: 16px; background: rgba(0,0,0,0.08);"></div>
572
-
573
- <!-- Right part: popover arrow reference -->
574
- <el-popover placement="bottom" :width="250" trigger="click" popper-class="list-popover-panel"
575
- :show-arrow="false" @before-enter="isUlVisible = true" @before-leave="isUlVisible = false">
576
- <template #reference>
577
- <div class="list-split-right-arrow" :style="{
578
- display: 'flex',
579
- alignItems: 'center',
580
- justifyContent: 'center',
581
- width: '24px',
582
- height: '100%',
583
- cursor: 'pointer',
584
- userSelect: 'none',
585
- borderRadius: '0 6px 6px 0',
586
- background: isUlVisible ? 'rgba(0,0,0,0.04)' : 'transparent'
587
- }">
588
- <span
589
- style="font-size: 9px; opacity: 0.6; display: inline-flex; align-items: center; justify-content: center; transition: transform 0.2s ease;"
590
- :style="{ transform: isUlVisible ? 'rotate(180deg)' : 'rotate(0deg)' }">▼</span>
591
- </div>
592
- </template>
593
- <!-- Unordered list presets grid -->
594
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 4px;">
595
- <div class="list-style-card"
596
- :class="{ active: isActiveListStyle ? isActiveListStyle('ul', 'disc') : false }"
597
- @click="emit('toggleListStyle', 'ul', 'disc', true)" @mousedown.prevent
598
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
599
- <img :src="ulDisc"
600
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
601
- </div>
602
- <div class="list-style-card"
603
- :class="{ active: isActiveListStyle ? isActiveListStyle('ul', '\'◆ \'') : false }"
604
- @click="emit('toggleListStyle', 'ul', '\'◆ \'', true)" @mousedown.prevent
605
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
606
- <img :src="ulDiamond"
607
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
608
- </div>
609
- <div class="list-style-card"
610
- :class="{ active: isActiveListStyle ? isActiveListStyle('ul', '\'▸ \'') : false }"
611
- @click="emit('toggleListStyle', 'ul', '\'▸ \'', true)" @mousedown.prevent
612
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
613
- <img :src="ulTriangle"
614
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
615
- </div>
616
- <div class="list-style-card"
617
- :class="{ active: isActiveListStyle ? isActiveListStyle('ul', '\'✔ \'') : false }"
618
- @click="emit('toggleListStyle', 'ul', '\'✔ \'', true)" @mousedown.prevent
619
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
620
- <img :src="ulCheckmark"
621
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
622
- </div>
623
- <div class="list-style-card"
624
- :class="{ active: isActiveListStyle ? isActiveListStyle('ul', 'square') : false }"
625
- @click="emit('toggleListStyle', 'ul', 'square', true)" @mousedown.prevent
626
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
627
- <img :src="ulSquare"
628
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
629
- </div>
630
- <div class="list-style-card"
631
- :class="{ active: isActiveListStyle ? isActiveListStyle('ul', 'circle') : false }"
632
- @click="emit('toggleListStyle', 'ul', 'circle', true)" @mousedown.prevent
633
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
634
- <img :src="ulCircle"
635
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
636
- </div>
637
- </div>
638
- </el-popover>
639
- </div>
640
-
641
- <!-- Ordered List Split Button -->
642
- <div class="list-split-btn-group" :style="{
643
- display: 'flex',
644
- alignItems: 'center',
645
- height: '32px',
646
- background: hasOrderedList ? '#e0ebff' : '#F5F6F7',
647
- borderRadius: '6px',
648
- color: hasOrderedList ? '#2563eb' : '#475569',
649
- fontSize: '12px',
650
- fontWeight: '500',
651
- flex: 1,
652
- overflow: 'hidden',
653
- border: '1px solid transparent'
654
- }">
655
- <!-- Left part: toggle list -->
656
- <div class="list-split-left-action" @click="emit('toggleListStyle', 'ol', 'decimal')" @mousedown.prevent
657
- style="
658
- display: flex;
659
- align-items: center;
660
- gap: 6px;
661
- padding: 0 8px;
662
- flex: 1;
663
- height: 100%;
664
- cursor: pointer;
665
- user-select: none;
666
- ">
667
- <svg class="icon" style="width: 14px; height: 14px; fill: currentColor;">
668
- <use xlink:href="#icon-youxuliebiao"></use>
669
- </svg>
670
- <span>有序列表</span>
671
- </div>
672
-
673
- <!-- Vertical separator line -->
674
- <div style="width: 1px; height: 16px; background: rgba(0,0,0,0.08);"></div>
675
-
676
- <!-- Right part: popover arrow reference -->
677
- <el-popover placement="bottom" :width="250" trigger="click" popper-class="list-popover-panel"
678
- :show-arrow="false" @before-enter="isOlVisible = true" @before-leave="isOlVisible = false">
679
- <template #reference>
680
- <div class="list-split-right-arrow" :style="{
681
- display: 'flex',
682
- alignItems: 'center',
683
- justifyContent: 'center',
684
- width: '24px',
685
- height: '100%',
686
- cursor: 'pointer',
687
- userSelect: 'none',
688
- borderRadius: '0 6px 6px 0',
689
- background: isOlVisible ? 'rgba(0,0,0,0.04)' : 'transparent'
690
- }">
691
- <span
692
- style="font-size: 9px; opacity: 0.6; display: inline-flex; align-items: center; justify-content: center; transition: transform 0.2s ease;"
693
- :style="{ transform: isOlVisible ? 'rotate(180deg)' : 'rotate(0deg)' }">▼</span>
694
- </div>
695
- </template>
696
- <!-- Ordered list presets grid -->
697
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 4px;">
698
- <div class="list-style-card"
699
- :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'decimal') : false }"
700
- @click="emit('toggleListStyle', 'ol', 'decimal', true)" @mousedown.prevent
701
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
702
- <img :src="olDicimal"
703
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
704
- </div>
705
- <div class="list-style-card"
706
- :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'circled-numbers') : false }"
707
- @click="emit('toggleListStyle', 'ol', 'circled-numbers', true)" @mousedown.prevent
708
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
709
- <img :src="olCircledNumbers"
710
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
711
- </div>
712
- <div class="list-style-card"
713
- :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'lower-alpha') : false }"
714
- @click="emit('toggleListStyle', 'ol', 'lower-alpha', true)" @mousedown.prevent
715
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
716
- <img :src="olLowerAlpha"
717
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
718
- </div>
719
- <div class="list-style-card"
720
- :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'upper-alpha') : false }"
721
- @click="emit('toggleListStyle', 'ol', 'upper-alpha', true)" @mousedown.prevent
722
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
723
- <img :src="olUpperAlpha"
724
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
725
- </div>
726
- <div class="list-style-card"
727
- :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'upper-roman') : false }"
728
- @click="emit('toggleListStyle', 'ol', 'upper-roman', true)" @mousedown.prevent
729
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
730
- <img :src="olUpperRoman"
731
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
732
- </div>
733
- <div class="list-style-card"
734
- :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'simp-chinese-informal') : false }"
735
- @click="emit('toggleListStyle', 'ol', 'simp-chinese-informal', true)" @mousedown.prevent
736
- style="display: flex; align-items: center; justify-content: center; height: 56px; padding: 4px;">
737
- <img :src="olChinese"
738
- style="max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none;" />
739
- </div>
740
- </div>
741
- </el-popover>
742
- </div>
743
- </div>
744
- </div>
745
- </div>
746
-
747
- <!-- Padding Accordion -->
748
- <div class="control-group shape-accordion-group" v-if="selectedElement && selectedElement.type !== 'group'">
749
- <div class="shape-accordion-header" @click="isPaddingExpanded = !isPaddingExpanded">
750
- <span class="control-label shape-accordion-header-label">
751
- <span class="accordion-arrow"
752
- :style="{ transform: isPaddingExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>内边距</span>
753
- </div>
754
-
755
- <div v-show="isPaddingExpanded" class="shape-accordion-content">
756
- <div style="display: flex; gap: 8px; align-items: stretch;">
757
- <!-- Grid of 4 inputs -->
758
- <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; flex: 1;">
759
- <!-- Padding Left (Top-Left) -->
760
- <div class="custom-num-input"
761
- style="height: 32px; padding: 0 8px; display: flex; align-items: center; background: #F5F6F7; border: none; border-radius: 4px;">
762
- <span
763
- style="font-size: 11px; color: var(--text-secondary, #64748b); margin-right: 6px; display: flex; align-items: center; opacity: 0.6;">
764
- <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5">
765
- <line x1="4" y1="4" x2="4" y2="20" />
766
- <rect x="8" y="5" width="12" height="14" stroke-dasharray="2 2" />
767
- </svg>
768
- </span>
769
- <input type="number" :value="paddingLeft"
770
- @input="emit('update:paddingLeft', Number(($event.target as HTMLInputElement).value))"
771
- class="custom-num-field" min="0" max="500"
772
- style="width: 100%; border: none; background: transparent; outline: none; text-align: left; font-size: 12px; color: var(--text-primary, #1e293b);" />
773
- </div>
774
-
775
- <!-- Padding Right (Top-Right) -->
776
- <div class="custom-num-input"
777
- style="height: 32px; padding: 0 8px; display: flex; align-items: center; background: #F5F6F7; border: none; border-radius: 4px;">
778
- <span
779
- style="font-size: 11px; color: var(--text-secondary, #64748b); margin-right: 6px; display: flex; align-items: center; opacity: 0.6;">
780
- <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5">
781
- <line x1="20" y1="4" x2="20" y2="20" />
782
- <rect x="4" y="5" width="12" height="14" stroke-dasharray="2 2" />
783
- </svg>
784
- </span>
785
- <input type="number" :value="paddingRight"
786
- @input="emit('update:paddingRight', Number(($event.target as HTMLInputElement).value))"
787
- class="custom-num-field" min="0" max="500"
788
- style="width: 100%; border: none; background: transparent; outline: none; text-align: left; font-size: 12px; color: var(--text-primary, #1e293b);" />
789
- </div>
790
-
791
- <!-- Padding Top (Bottom-Left) -->
792
- <div class="custom-num-input"
793
- style="height: 32px; padding: 0 8px; display: flex; align-items: center; background: #F5F6F7; border: none; border-radius: 4px;">
794
- <span
795
- style="font-size: 11px; color: var(--text-secondary, #64748b); margin-right: 6px; display: flex; align-items: center; opacity: 0.6;">
796
- <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5">
797
- <line x1="4" y1="4" x2="20" y2="4" />
798
- <rect x="5" y="8" width="14" height="12" stroke-dasharray="2 2" />
799
- </svg>
800
- </span>
801
- <input type="number" :value="paddingTop"
802
- @input="emit('update:paddingTop', Number(($event.target as HTMLInputElement).value))"
803
- class="custom-num-field" min="0" max="500"
804
- style="width: 100%; border: none; background: transparent; outline: none; text-align: left; font-size: 12px; color: var(--text-primary, #1e293b);" />
805
- </div>
806
-
807
- <!-- Padding Bottom (Bottom-Right) -->
808
- <div class="custom-num-input"
809
- style="height: 32px; padding: 0 8px; display: flex; align-items: center; background: #F5F6F7; border: none; border-radius: 4px;">
810
- <span
811
- style="font-size: 11px; color: var(--text-secondary, #64748b); margin-right: 6px; display: flex; align-items: center; opacity: 0.6;">
812
- <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5">
813
- <line x1="4" y1="20" x2="20" y2="20" />
814
- <rect x="5" y="4" width="14" height="12" stroke-dasharray="2 2" />
815
- </svg>
816
- </span>
817
- <input type="number" :value="paddingBottom"
818
- @input="emit('update:paddingBottom', Number(($event.target as HTMLInputElement).value))"
819
- class="custom-num-field" min="0" max="500"
820
- style="width: 100%; border: none; background: transparent; outline: none; text-align: left; font-size: 12px; color: var(--text-primary, #1e293b);" />
821
- </div>
822
- </div>
823
-
824
- <!-- Link/Lock button -->
825
- <div class="grid-btn" title="锁定所有边框内边距一致" @click="emit('update:isPaddingLocked', !isPaddingLocked)"
826
- :style="isPaddingLocked ? { background: 'var(--accent-primary, #6366f1)', color: '#ffffff' } : { background: '#F5F6F7', color: 'var(--text-secondary, #64748b)' }"
827
- style="width: 32px; height: 32px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; box-sizing: border-box; flex-shrink: 0; border: none;">
828
- <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
829
- <use :xlink:href="isPaddingLocked ? '#icon-suoding1' : '#icon-quxiaosuoding'"></use>
830
- </svg>
831
- </div>
832
- </div>
833
- </div>
834
- </div>
835
-
836
- <!-- Divider above Text Outline Accordion -->
837
- <div class="control-divider" style="margin: 12px 0 4px; border-top: 1px solid var(--border-color, #E2E8F0);"></div>
838
-
839
- <!-- Text Outline Accordion -->
840
- <div class="control-group shape-accordion-group">
841
- <div class="shape-accordion-header" @click="toggleTextBorderHeader">
842
- <span class="control-label shape-accordion-header-label">
843
- <span class="accordion-arrow"
844
- :style="{ transform: (enableTextStroke && isTextBorderExpanded) ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>文字轮廓</span>
845
- <el-switch :model-value="enableTextStroke" size="small" @click.stop
846
- @change="emit('update:enableTextStroke', $event); isTextBorderExpanded = $event" />
847
- </div>
848
-
849
- <div v-show="isTextBorderExpanded && enableTextStroke" class="shape-accordion-content">
850
- <!-- Color Row -->
851
- <div class="shape-property-row">
852
- <span class="shape-property-label">颜色</span>
853
- <QuickColorPopover v-model:visible="textStrokePopoverVisible" :color="textStrokeColor || '#000000'"
854
- :recent-colors="recentColors" @select-color="emit('update:textStrokeColor', $event)">
855
- <template #reference>
856
- <div class="color-picker-pill-wrapper">
857
- <div style="display: flex; align-items: center; gap: 8px;">
858
- <div :style="{ background: textStrokeColor || '#000000' }"
859
- style="width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.15);">
860
- </div>
861
- <span
862
- style="font-size: 11px; color: var(--text-primary, #1e293b); font-family: monospace; font-weight: 500;">{{
863
- (textStrokeColor || '#000000').replace('#', '').toUpperCase() }}</span>
864
- </div>
865
- </div>
866
- </template>
867
- </QuickColorPopover>
868
- </div>
869
-
870
- <!-- Style & Width Row -->
871
- <div class="shape-property-row">
872
- <span class="shape-property-label">样式</span>
873
- <div class="shape-border-style-wrapper">
874
- <!-- Style dropdown -->
875
- <el-select :model-value="textStrokeStyle" @update:model-value="emit('update:textStrokeStyle', $event)"
876
- size="small" class="custom-select" style="width: 96px; flex-shrink: 0;">
877
- <el-option label="───" value="solid" />
878
- <el-option label="---" value="dashed" />
879
- </el-select>
880
- <!-- Width selector -->
881
- <div class="shape-border-width-wrapper" style="width: 96px; flex: none;">
882
- <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"
883
- style="margin-right: 6px; color: var(--text-secondary, #64748b); flex-shrink: 0;">
884
- <line x1="3" y1="6" x2="21" y2="6" />
885
- <line x1="3" y1="12" x2="21" y2="12" />
886
- <line x1="3" y1="18" x2="21" y2="18" />
887
- </svg>
888
- <input type="number" :value="textStrokeWidth"
889
- @input="emit('update:textStrokeWidth', Number(($event.target as HTMLInputElement).value))" :min="1"
890
- :max="20" class="shape-num-field" />
891
- </div>
892
- </div>
893
- </div>
894
-
895
- <!-- Transparency Row -->
896
- <div class="shape-property-row">
897
- <span class="shape-property-label">透明度</span>
898
- <div class="shape-slider-wrapper">
899
- <el-slider :model-value="textStrokeAlpha" @update:model-value="emit('update:textStrokeAlpha', $event)"
900
- :min="0" :max="100" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
901
- :show-input-controls="false" />
902
- </div>
903
- </div>
904
- </div>
905
- </div>
906
-
907
- <!-- Shadow Accordion -->
908
- <div class="control-group shape-accordion-group">
909
- <div class="shape-accordion-header" @click="toggleTextShadowHeader">
910
- <span class="control-label shape-accordion-header-label">
911
- <span class="accordion-arrow"
912
- :style="{ transform: (enableTextShadow && isTextShadowExpanded) ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>阴影</span>
913
- <el-switch :model-value="enableTextShadow" size="small" @click.stop
914
- @change="emit('update:enableTextShadow', $event); isTextShadowExpanded = $event" />
915
- </div>
916
-
917
- <div v-show="isTextShadowExpanded && enableTextShadow" class="shape-accordion-content">
918
- <!-- Color Row -->
919
- <div class="shape-property-row">
920
- <span class="shape-property-label">阴影颜色</span>
921
- <QuickColorPopover v-model:visible="textShadowPopoverVisible" :color="textShadowHex || '#000000'"
922
- :recent-colors="recentColors" @select-color="emit('update:textShadowHex', $event)">
923
- <template #reference>
924
- <div class="color-picker-pill-wrapper">
925
- <div style="display: flex; align-items: center; gap: 8px;">
926
- <div :style="{ background: textShadowHex || '#000000' }"
927
- style="width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.15);">
928
- </div>
929
- <span
930
- style="font-size: 11px; color: var(--text-primary, #1e293b); font-family: monospace; font-weight: 500;">{{
931
- (textShadowHex || '#000000').replace('#', '').toUpperCase() }}</span>
932
- </div>
933
- </div>
934
- </template>
935
- </QuickColorPopover>
936
- </div>
937
-
938
- <!-- Angle Row -->
939
- <div class="shape-property-row">
940
- <span class="shape-property-label">角度</span>
941
- <div style="display: flex; align-items: center; gap: 8px; flex: 1; justify-content: flex-end;">
942
- <div class="shape-border-width-wrapper" style="position: relative;">
943
- <input type="number" :value="textShadowAngle"
944
- @input="emit('update:textShadowAngle', Number(($event.target as HTMLInputElement).value))"
945
- class="shape-num-field" />
946
- <span class="angle-icon"
947
- :style="{ left: `calc(8px + ${String(textShadowAngle ?? 0).length * 8}px)` }">°</span>
948
- </div>
949
- <!-- Dial Picker -->
950
- <div class="angle-dial" @mousedown="startDragAngle">
951
- <div :style="{ transform: `rotate(${textShadowAngle || 0}deg)` }" class="dial-needle">
952
- <div class="dial-needle-tip"></div>
953
- </div>
954
- <div class="dial-center-dot"></div>
955
- </div>
956
- </div>
957
- </div>
958
-
959
- <!-- Size Row -->
960
- <div class="shape-property-row">
961
- <span class="shape-property-label">大小</span>
962
- <div class="shape-slider-wrapper">
963
- <el-slider :model-value="textShadowSize" @update:model-value="emit('update:textShadowSize', $event)"
964
- :min="0" :max="200" :step="1" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false"
965
- show-input :show-input-controls="false" />
966
- </div>
967
- </div>
968
-
969
- <!-- Blur Row -->
970
- <div class="shape-property-row">
971
- <span class="shape-property-label">模糊</span>
972
- <div class="shape-slider-wrapper">
973
- <el-slider :model-value="textShadowBlur" @update:model-value="emit('update:textShadowBlur', $event)"
974
- :min="0" :max="267" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
975
- :show-input-controls="false" />
976
- </div>
977
- </div>
978
-
979
- <!-- Distance Row -->
980
- <div class="shape-property-row">
981
- <span class="shape-property-label">距离</span>
982
- <div class="shape-slider-wrapper">
983
- <el-slider :model-value="textShadowDistance" @update:model-value="emit('update:textShadowDistance', $event)"
984
- :min="0" :max="267" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
985
- :show-input-controls="false" />
986
- </div>
987
- </div>
988
-
989
- <!-- Transparency Row -->
990
- <div class="shape-property-row">
991
- <span class="shape-property-label">透明度</span>
992
- <div class="shape-slider-wrapper">
993
- <el-slider :model-value="textShadowAlpha" @update:model-value="emit('update:textShadowAlpha', $event)"
994
- :min="0" :max="100" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
995
- :show-input-controls="false" />
996
- </div>
997
- </div>
998
- </div>
999
- </div>
1000
-
1001
- <!-- Reflection Accordion -->
1002
- <div class="control-group shape-accordion-group">
1003
- <div class="shape-accordion-header" @click="toggleTextReflectionHeader">
1004
- <span class="control-label shape-accordion-header-label">
1005
- <span class="accordion-arrow"
1006
- :style="{ transform: (enableTextReflection && isTextReflectionExpanded) ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>倒影</span>
1007
- <el-switch :model-value="enableTextReflection" size="small" @click.stop
1008
- @change="emit('update:enableTextReflection', $event); isTextReflectionExpanded = $event" />
1009
- </div>
1010
-
1011
- <div v-show="isTextReflectionExpanded && enableTextReflection" class="shape-accordion-content">
1012
- <!-- Distance Row -->
1013
- <div class="shape-property-row">
1014
- <span class="shape-property-label">距离</span>
1015
- <div class="shape-slider-wrapper">
1016
- <el-slider :model-value="textReflectionDistance"
1017
- @update:model-value="emit('update:textReflectionDistance', $event)" :min="0" :max="100" :step="0.1"
1018
- size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1019
- :show-input-controls="false" />
1020
- </div>
1021
- </div>
1022
-
1023
- <!-- Size Row -->
1024
- <div class="shape-property-row">
1025
- <span class="shape-property-label">大小</span>
1026
- <div class="shape-slider-wrapper">
1027
- <el-slider :model-value="textReflectionSize" @update:model-value="emit('update:textReflectionSize', $event)"
1028
- :min="0" :max="100" :step="0.1" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false"
1029
- show-input :show-input-controls="false" />
1030
- </div>
1031
- </div>
1032
-
1033
- <!-- Opacity Row -->
1034
- <div class="shape-property-row">
1035
- <span class="shape-property-label">透明度</span>
1036
- <div class="shape-slider-wrapper">
1037
- <el-slider :model-value="textReflectionOpacity"
1038
- @update:model-value="emit('update:textReflectionOpacity', $event)" :min="0" :max="100" :step="1"
1039
- size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1040
- :show-input-controls="false" />
1041
- </div>
1042
- </div>
1043
-
1044
- <!-- Blur Row -->
1045
- <div class="shape-property-row">
1046
- <span class="shape-property-label">模糊</span>
1047
- <div class="shape-slider-wrapper">
1048
- <el-slider :model-value="textReflectionBlur" @update:model-value="emit('update:textReflectionBlur', $event)"
1049
- :min="0" :max="100" :step="0.1" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false"
1050
- show-input :show-input-controls="false" />
1051
- </div>
1052
- </div>
1053
- </div>
1054
- </div>
1055
- </div>
1056
- </template>
1057
-
1058
- <style scoped lang="scss">
1059
- .shape-property-row {
1060
- display: flex;
1061
- align-items: center;
1062
- justify-content: space-between;
1063
- height: 32px;
1064
- padding: 0;
1065
- gap: 8px;
1066
- }
1067
-
1068
- .shape-property-label {
1069
- font-size: 12px;
1070
- color: var(--text-secondary, #64748b);
1071
- width: 80px;
1072
- flex-shrink: 0;
1073
- text-align: left;
1074
- }
1075
-
1076
- .font-action-btn-clean {
1077
- flex: 1;
1078
- height: 100%;
1079
- border: none;
1080
- background: transparent;
1081
- border-radius: 4px;
1082
- cursor: pointer;
1083
- display: flex;
1084
- align-items: center;
1085
- justify-content: center;
1086
- font-size: 13px;
1087
- color: var(--text-secondary, #64748b);
1088
- transition: all 0.15s ease;
1089
- padding: 0;
1090
- user-select: none;
1091
-
1092
- &:hover {
1093
- background: rgba(255, 255, 255, 0.7);
1094
- }
1095
- }
1096
-
1097
- .font-action-btn-rows {
1098
- padding: 4px 6px;
1099
- display: flex;
1100
- align-items: center;
1101
- background: #F5F6F7;
1102
- border-radius: 4px;
1103
- height: 32px;
1104
- box-sizing: border-box;
1105
- flex: 1;
1106
- justify-content: space-between;
1107
- gap: 4px;
1108
- }
1109
-
1110
- .font-styles {
1111
- background: #F5F6F7 !important;
1112
- padding: 4px 6px !important;
1113
- border-radius: 4px !important;
1114
- height: 32px !important;
1115
- box-sizing: border-box !important;
1116
- flex: 1 !important;
1117
- gap: 4px !important;
1118
-
1119
- .font-action-btn-clean {
1120
- flex: 1 !important;
1121
- height: 100% !important;
1122
- width: auto !important;
1123
- border-radius: 4px !important;
1124
-
1125
- &:hover {
1126
- background: rgba(255, 255, 255, 0.6) !important;
1127
- }
1128
- }
1129
- }
1130
-
1131
- .custom-num-input {
1132
- display: flex;
1133
- align-items: center;
1134
- background: #F5F6F7;
1135
- border: 1px solid #F5F6F7;
1136
- border-radius: 4px;
1137
- padding: 0 8px;
1138
- height: 32px;
1139
- box-sizing: border-box;
1140
- transition: border-color 0.15s ease;
1141
- }
1142
-
1143
- .custom-num-input:focus-within {
1144
- border-color: var(--accent-primary, #6366f1);
1145
- }
1146
-
1147
- .custom-num-field {
1148
- width: 100%;
1149
- border: none;
1150
- background: transparent;
1151
- outline: none;
1152
- font-size: 12px;
1153
- color: var(--text-primary, #1e293b);
1154
- padding: 0 4px;
1155
-
1156
- &::-webkit-outer-spin-button,
1157
- &::-webkit-inner-spin-button {
1158
- -webkit-appearance: none;
1159
- margin: 0;
1160
- }
1161
- }
1162
-
1163
- .color-picker-cell-wrapper {
1164
- flex: 1;
1165
- display: flex;
1166
- width: 100%;
1167
-
1168
- :deep(.quick-color-popover-trigger),
1169
- :deep(.color-span-box),
1170
- :deep(.el-popover__reference-wrapper) {
1171
- display: flex !important;
1172
- flex: 1 !important;
1173
- width: 100% !important;
1174
- }
1175
- }
1176
-
1177
- .color-picker-pill-wrapper {
1178
- position: relative;
1179
- display: flex;
1180
- align-items: center;
1181
- justify-content: space-between;
1182
- background: #F5F6F7;
1183
- border-radius: 4px;
1184
- padding: 0 10px;
1185
- height: 32px;
1186
- width: 100%;
1187
- flex: 1;
1188
- cursor: pointer;
1189
- border: none;
1190
- box-sizing: border-box;
1191
- }
1192
-
1193
- .shape-accordion-group {
1194
- margin-top: 2px;
1195
- }
1196
-
1197
-
1198
- .list-split-btn-group {
1199
- transition: border-color 0.2s, background-color 0.2s;
1200
-
1201
- &:hover {
1202
- border-color: #cbd5e1;
1203
- }
1204
- }
1205
-
1206
- .list-split-left-action {
1207
- transition: background-color 0.2s;
1208
-
1209
- &:hover {
1210
- background-color: rgba(0, 0, 0, 0.04);
1211
- }
1212
-
1213
- &:active {
1214
- background-color: rgba(0, 0, 0, 0.08);
1215
- }
1216
- }
1217
-
1218
- .list-split-right-arrow {
1219
- transition: background-color 0.2s;
1220
-
1221
- &:hover {
1222
- background-color: rgba(0, 0, 0, 0.04);
1223
- }
1224
-
1225
- &:active {
1226
- background-color: rgba(0, 0, 0, 0.08);
1227
- }
1228
- }
1229
-
1230
- .shape-accordion-header {
1231
- height: 34px;
1232
- padding: 0 16px;
1233
- display: flex;
1234
- align-items: center;
1235
- gap: 8px;
1236
- cursor: pointer;
1237
- user-select: none;
1238
- }
1239
-
1240
- .shape-accordion-header-label {
1241
- font-size: 13px;
1242
- font-weight: 700;
1243
- color: var(--text-primary, #0f172a);
1244
- display: flex;
1245
- align-items: center;
1246
- gap: 6px;
1247
- width: 80px;
1248
- flex-shrink: 0;
1249
- text-align: left;
1250
- }
1251
-
1252
- .accordion-arrow {
1253
- font-size: 9px;
1254
- transition: transform 0.2s;
1255
- display: inline-block;
1256
- color: #0f172a;
1257
- transform-origin: center;
1258
- }
1259
-
1260
- .shape-accordion-content {
1261
- padding: 8px 16px 4px 16px;
1262
- display: flex;
1263
- flex-direction: column;
1264
- gap: 12px;
1265
- }
1266
-
1267
- .shape-border-style-wrapper {
1268
- display: flex;
1269
- align-items: center;
1270
- justify-content: flex-end;
1271
- gap: 8px;
1272
- flex: 1;
1273
- }
1274
-
1275
- .shape-border-width-wrapper {
1276
- display: flex;
1277
- align-items: center;
1278
- background: #F5F6F7;
1279
- border: none;
1280
- border-radius: 4px;
1281
- padding: 0 8px;
1282
- height: 32px;
1283
- box-sizing: border-box;
1284
- position: relative;
1285
- }
1286
-
1287
- .shape-num-field {
1288
- border: none;
1289
- background: transparent;
1290
- outline: none;
1291
- font-size: 12px;
1292
- color: var(--text-primary, #1e293b);
1293
- width: 100%;
1294
- text-align: left;
1295
-
1296
- &::-webkit-outer-spin-button,
1297
- &::-webkit-inner-spin-button {
1298
- -webkit-appearance: none;
1299
- margin: 0;
1300
- }
1301
- }
1302
-
1303
- .angle-icon {
1304
- position: absolute;
1305
- top: 50%;
1306
- transform: translateY(calc(-50% - 2px));
1307
- pointer-events: none;
1308
- font-size: 12px;
1309
- color: var(--text-secondary, #64748b);
1310
- margin-left: 2px;
1311
- }
1312
-
1313
- .angle-dial {
1314
- width: 32px;
1315
- height: 32px;
1316
- border: 1px solid var(--border-color, #E2E8F0);
1317
- border-radius: 50%;
1318
- display: flex;
1319
- align-items: center;
1320
- justify-content: center;
1321
- cursor: pointer;
1322
- position: relative;
1323
- background: #ffffff;
1324
- flex-shrink: 0;
1325
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
1326
- user-select: none;
1327
- }
1328
-
1329
- .dial-needle {
1330
- width: 100%;
1331
- height: 2px;
1332
- position: absolute;
1333
- transform-origin: center;
1334
- display: flex;
1335
- justify-content: flex-end;
1336
- align-items: center;
1337
- }
1338
-
1339
- .dial-needle-tip {
1340
- width: 16px;
1341
- height: 2px;
1342
- background: #6F7588;
1343
- }
1344
-
1345
- .dial-center-dot {
1346
- position: absolute;
1347
- width: 4px;
1348
- height: 4px;
1349
- background-color: #6F7588;
1350
- border-radius: 50%;
1351
- top: 50%;
1352
- left: 50%;
1353
- transform: translate(-50%, -50%);
1354
- pointer-events: none;
1355
- }
1356
-
1357
- .shape-slider-wrapper {
1358
- display: flex;
1359
- align-items: center;
1360
- gap: 10px;
1361
- flex: 1;
1362
- }
1363
-
1364
- .custom-slider {
1365
- --el-slider-height: 4px !important;
1366
- --el-slider-button-size: 16px !important;
1367
- --el-slider-button-wrapper-offset: -16px !important;
1368
-
1369
- :deep(.show-input) {
1370
- margin-right: 16px;
1371
- }
1372
-
1373
- :deep(.el-input-number) {
1374
- width: 48px;
1375
- height: 30px;
1376
-
1377
- .el-input__wrapper {
1378
- padding: 4px;
1379
- background: #F5F6F7;
1380
- border: none;
1381
- border-radius: 4px;
1382
- box-shadow: none;
1383
- }
1384
-
1385
- .el-input__inner {
1386
- text-align: center;
1387
- font-size: 12px;
1388
- color: var(--text-primary, #1e293b);
1389
- }
1390
- }
1391
- }
1392
-
1393
- :deep(.custom-select) {
1394
- .el-select__wrapper {
1395
- background-color: #F5F6F7 !important;
1396
- border: none !important;
1397
- box-shadow: none !important;
1398
- height: 32px !important;
1399
- min-height: 32px !important;
1400
- line-height: 32px !important;
1401
- padding: 0 10px !important;
1402
- border-radius: 4px !important;
1403
-
1404
- &.is-focused {
1405
- box-shadow: 0 0 0 1px var(--accent-primary, #6366f1) inset !important;
1406
- }
1407
- }
1408
-
1409
- .el-select__selected-item {
1410
- font-size: 12px !important;
1411
- color: var(--text-primary, #1e293b) !important;
1412
- }
1413
- }
1414
-
1415
-
1416
- :deep(.el-switch__core) {
1417
- border-radius: 4px !important;
1418
- }
1419
-
1420
- :deep(.el-switch__action) {
1421
- border-radius: 3px !important;
1422
- }
1423
-
1424
- :deep(.color-span-box) {
1425
- width: 100%;
1426
- }
1427
- </style>
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { getFontVariants } from '../../../configs/fonts'
4
+ import QuickColorPopover from '../../QuickColorPopover.vue'
5
+ import { type SlideTextType, TEXT_TYPE_LIST } from '../../../utils/textTypes'
6
+
7
+ import ulDisc from '../../../assets/list/ul-disc.png'
8
+ import ulDiamond from '../../../assets/list/ul-diamond.png'
9
+ import ulTriangle from '../../../assets/list/ul-triangle.png'
10
+ import ulCheckmark from '../../../assets/list/ul-checkmark.png'
11
+ import ulSquare from '../../../assets/list/ul-square.png'
12
+ import ulCircle from '../../../assets/list/ul-circle.png'
13
+ import olDicimal from '../../../assets/list/ol-dicimal.png'
14
+ import olCircledNumbers from '../../../assets/list/ol-circled-numbers.png'
15
+ import olLowerAlpha from '../../../assets/list/ol-lower-alpha.png'
16
+ import olUpperAlpha from '../../../assets/list/ol-upper-alpha.png'
17
+ import olUpperRoman from '../../../assets/list/ol-upper-roman.png'
18
+ import olChinese from '../../../assets/list/ol-chinese.png'
19
+
20
+ const props = withDefaults(defineProps<{
21
+ selectedElement: any
22
+ selectedElementIds?: string[]
23
+ currentTextType?: SlideTextType
24
+ sysFonts?: Array<{ label: string; value: string }>
25
+ themeRecipe?: any
26
+ fontStyleSupport?: { bold: boolean; italic: boolean; boldItalic: boolean }
27
+ styleFontFamily?: string
28
+ styleFontSize?: string
29
+ styleFontWeightStyle?: string
30
+ lineHeightNumber?: number
31
+ letterSpacingNumber?: number
32
+ styleTextColor?: string
33
+ styleTextBackground?: string
34
+ styleTextAlign?: string
35
+ styleVerticalAlign?: string
36
+ styleWritingMode?: string
37
+ styleAutoFitType?: string
38
+ styleWordWrap?: boolean
39
+ paddingTop?: number
40
+ paddingRight?: number
41
+ paddingBottom?: number
42
+ paddingLeft?: number
43
+ isPaddingLocked?: boolean
44
+ hasUnorderedList?: boolean
45
+ hasOrderedList?: boolean
46
+ isActiveListStyle?: (type: any, val: string) => boolean
47
+ enableTextStroke?: boolean
48
+ textStrokeColor?: string
49
+ textStrokeWidth?: number
50
+ textStrokeStyle?: string
51
+ textStrokeAlpha?: number
52
+ enableTextShadow?: boolean
53
+ textShadowHex?: string
54
+ textShadowAlpha?: number
55
+ textShadowAngle?: number
56
+ textShadowSize?: number
57
+ textShadowDistance?: number
58
+ textShadowBlur?: number
59
+ enableTextReflection?: boolean
60
+ textReflectionDistance?: number
61
+ textReflectionSize?: number
62
+ textReflectionOpacity?: number
63
+ textReflectionBlur?: number
64
+ recentColors?: string[]
65
+ getStyleVal?: (prop: string, fallback?: any) => any
66
+ }>(), {
67
+ selectedElementIds: () => [],
68
+ currentTextType: 'normalText',
69
+ sysFonts: () => [],
70
+ fontStyleSupport: () => ({ bold: true, italic: true, boldItalic: true }),
71
+ styleFontFamily: '默认字体',
72
+ styleFontSize: '16px',
73
+ styleFontWeightStyle: 'Regular',
74
+ lineHeightNumber: 1.3,
75
+ letterSpacingNumber: 0,
76
+ styleTextColor: '#000000',
77
+ styleTextBackground: 'transparent',
78
+ styleTextAlign: 'center',
79
+ styleVerticalAlign: 'middle',
80
+ styleWritingMode: 'horizontal-tb',
81
+ styleAutoFitType: 'shape',
82
+ styleWordWrap: true,
83
+ paddingTop: 4.8,
84
+ paddingRight: 9.6,
85
+ paddingBottom: 4.8,
86
+ paddingLeft: 9.6,
87
+ isPaddingLocked: false,
88
+ hasUnorderedList: false,
89
+ hasOrderedList: false,
90
+ enableTextStroke: false,
91
+ textStrokeColor: '#000000',
92
+ textStrokeWidth: 1,
93
+ textStrokeStyle: 'solid',
94
+ textStrokeAlpha: 100,
95
+ enableTextShadow: false,
96
+ textShadowHex: '#000000',
97
+ textShadowAlpha: 50,
98
+ textShadowAngle: 45,
99
+ textShadowSize: 100,
100
+ textShadowDistance: 3,
101
+ textShadowBlur: 4,
102
+ enableTextReflection: false,
103
+ textReflectionDistance: 0,
104
+ textReflectionSize: 100,
105
+ textReflectionOpacity: 10,
106
+ textReflectionBlur: 0,
107
+ recentColors: () => [],
108
+ getStyleVal: () => undefined
109
+ })
110
+
111
+ const emit = defineEmits<{
112
+ (e: 'changeTextType', val: SlideTextType): void
113
+ (e: 'changeFontFamily', val: string): void
114
+ (e: 'changeFontSize', val: string): void
115
+ (e: 'update:styleFontWeightStyle', val: string): void
116
+ (e: 'update:lineHeightNumber', val: number): void
117
+ (e: 'update:letterSpacingNumber', val: number): void
118
+ (e: 'update:styleTextColor', val: string): void
119
+ (e: 'update:styleTextBackground', val: string): void
120
+ (e: 'update:styleTextAlign', val: string): void
121
+ (e: 'update:styleVerticalAlign', val: string): void
122
+ (e: 'update:styleWritingMode', val: string): void
123
+ (e: 'update:styleAutoFitType', val: string): void
124
+ (e: 'update:styleWordWrap', val: boolean): void
125
+ (e: 'update:paddingTop', val: number): void
126
+ (e: 'update:paddingRight', val: number): void
127
+ (e: 'update:paddingBottom', val: number): void
128
+ (e: 'update:paddingLeft', val: number): void
129
+ (e: 'update:isPaddingLocked', val: boolean): void
130
+ (e: 'toggleListStyle', type: string, val: string, isFromPanel?: boolean): void
131
+ (e: 'update:enableTextStroke', val: boolean): void
132
+ (e: 'update:textStrokeColor', val: string): void
133
+ (e: 'update:textStrokeWidth', val: number): void
134
+ (e: 'update:textStrokeStyle', val: string): void
135
+ (e: 'update:textStrokeAlpha', val: number): void
136
+ (e: 'update:enableTextShadow', val: boolean): void
137
+ (e: 'update:textShadowHex', val: string): void
138
+ (e: 'update:textShadowAlpha', val: number): void
139
+ (e: 'update:textShadowAngle', val: number): void
140
+ (e: 'update:textShadowSize', val: number): void
141
+ (e: 'update:textShadowBlur', val: number): void
142
+ (e: 'update:textShadowDistance', val: number): void
143
+ (e: 'update:enableTextReflection', val: boolean): void
144
+ (e: 'update:textReflectionDistance', val: number): void
145
+ (e: 'update:textReflectionSize', val: number): void
146
+ (e: 'update:textReflectionOpacity', val: number): void
147
+ (e: 'update:textReflectionBlur', val: number): void
148
+ (e: 'toggleBold'): void
149
+ (e: 'toggleItalic'): void
150
+ (e: 'toggleUnderline'): void
151
+ (e: 'toggleStrikethrough'): void
152
+ }>()
153
+
154
+ const isListExpanded = ref(true)
155
+ const isPaddingExpanded = ref(true)
156
+ const isTextBorderExpanded = ref(true)
157
+ const isTextShadowExpanded = ref(true)
158
+ const isTextReflectionExpanded = ref(true)
159
+
160
+ const isUlVisible = ref(false)
161
+ const isOlVisible = ref(false)
162
+ const textColorPopoverVisible = ref(false)
163
+ const textBgPopoverVisible = ref(false)
164
+ const textStrokePopoverVisible = ref(false)
165
+ const textShadowPopoverVisible = ref(false)
166
+ const isAutoFitPopoverVisible = ref(false)
167
+ const autoFitTriggerRef = ref<HTMLElement | null>(null)
168
+ const autoFitTriggerWidth = ref(248)
169
+
170
+ const autoFitOptions = [
171
+ { label: '文字溢出不自动调整', value: 'none' },
172
+ { label: '文字溢出缩小文字', value: 'text' },
173
+ { label: '文字溢出调整形状大小', value: 'shape' }
174
+ ]
175
+
176
+ const currentAutoFitLabel = computed(() => {
177
+ const match = autoFitOptions.find(opt => opt.value === props.styleAutoFitType)
178
+ return match ? match.label : '文字溢出不自动调整'
179
+ })
180
+
181
+ function onAutoFitPopoverShow() {
182
+ if (autoFitTriggerRef.value) {
183
+ autoFitTriggerWidth.value = autoFitTriggerRef.value.offsetWidth
184
+ }
185
+ }
186
+
187
+ function selectAutoFitType(val: string) {
188
+ emit('update:styleAutoFitType', val)
189
+ isAutoFitPopoverVisible.value = false
190
+ }
191
+
192
+ const isWritingModePopoverVisible = ref(false)
193
+ const writingModeTriggerRef = ref<HTMLElement | null>(null)
194
+ const writingModeTriggerWidth = ref(248)
195
+
196
+ const writingModeOptions = [
197
+ { label: '横向', value: 'horizontal-tb' },
198
+ { label: '竖向(从左到右)', value: 'vertical-lr' },
199
+ { label: '竖向(从右到左)', value: 'vertical-rl' }
200
+ ]
201
+
202
+ const currentWritingModeLabel = computed(() => {
203
+ const match = writingModeOptions.find(opt => opt.value === props.styleWritingMode)
204
+ return match ? match.label : '横向'
205
+ })
206
+
207
+ function onWritingModePopoverShow() {
208
+ if (writingModeTriggerRef.value) {
209
+ writingModeTriggerWidth.value = writingModeTriggerRef.value.offsetWidth
210
+ }
211
+ }
212
+
213
+ function selectWritingMode(val: string) {
214
+ emit('update:styleWritingMode', val)
215
+ isWritingModePopoverVisible.value = false
216
+ }
217
+
218
+ // Text Classification (类型) Popover
219
+ const isTextTypePopoverVisible = ref(false)
220
+ const textTypeTriggerRef = ref<HTMLElement | null>(null)
221
+ const textTypeTriggerWidth = ref(248)
222
+
223
+ const currentTextTypeLabel = computed(() => {
224
+ const match = TEXT_TYPE_LIST.find(item => item.type === props.currentTextType)
225
+ return match ? `${match.label}(${match.enLabel})` : '正文文本(Normal Text)'
226
+ })
227
+
228
+ function onTextTypePopoverShow() {
229
+ if (textTypeTriggerRef.value) {
230
+ textTypeTriggerWidth.value = textTypeTriggerRef.value.offsetWidth
231
+ }
232
+ }
233
+
234
+ function selectTextType(val: SlideTextType) {
235
+ emit('changeTextType', val)
236
+ isTextTypePopoverVisible.value = false
237
+ }
238
+
239
+ // Font Weight / Style (样式) Popover
240
+ const isFontWeightPopoverVisible = ref(false)
241
+ const fontWeightTriggerRef = ref<HTMLElement | null>(null)
242
+ const fontWeightTriggerWidth = ref(120)
243
+
244
+ const currentFontWeightLabel = computed(() => {
245
+ const match = currentFontVariants.value.find(v => v.value === props.styleFontWeightStyle)
246
+ return match ? match.label : (props.styleFontWeightStyle || 'Regular')
247
+ })
248
+
249
+ function onFontWeightPopoverShow() {
250
+ if (fontWeightTriggerRef.value) {
251
+ fontWeightTriggerWidth.value = fontWeightTriggerRef.value.offsetWidth
252
+ }
253
+ }
254
+
255
+ function selectFontWeight(val: string) {
256
+ emit('update:styleFontWeightStyle', val)
257
+ isFontWeightPopoverVisible.value = false
258
+ }
259
+
260
+ const fontSizeClean = computed(() => {
261
+ return String(props.styleFontSize || '16').replace('px', '')
262
+ })
263
+
264
+ // Font Size (字号) Popover
265
+ const isFontSizePopoverVisible = ref(false)
266
+ const fontSizeTriggerRef = ref<HTMLElement | null>(null)
267
+ const fontSizeTriggerWidth = ref(120)
268
+
269
+ function onFontSizePopoverShow() {
270
+ if (fontSizeTriggerRef.value) {
271
+ fontSizeTriggerWidth.value = fontSizeTriggerRef.value.offsetWidth
272
+ }
273
+ }
274
+
275
+ function selectFontSize(val: string) {
276
+ emit('changeFontSize', val)
277
+ isFontSizePopoverVisible.value = false
278
+ }
279
+
280
+ function onFontSizeInput(e: Event) {
281
+ const target = e.target as HTMLInputElement
282
+ const raw = target.value.trim()
283
+ if (raw) {
284
+ emit('changeFontSize', raw)
285
+ }
286
+ }
287
+
288
+ function onFontSizeEnter(e: KeyboardEvent) {
289
+ const target = e.target as HTMLInputElement
290
+ const raw = target.value.trim()
291
+ if (raw) {
292
+ emit('changeFontSize', raw)
293
+ }
294
+ isFontSizePopoverVisible.value = false
295
+ target.blur()
296
+ }
297
+
298
+ function onFontSizeBlur(e: FocusEvent) {
299
+ const target = e.target as HTMLInputElement
300
+ const raw = target.value.trim()
301
+ if (raw) {
302
+ emit('changeFontSize', raw)
303
+ }
304
+ }
305
+
306
+ function checkStyleVal(prop: string, fallback?: any): any {
307
+ if (props.getStyleVal) {
308
+ const val = props.getStyleVal(prop, fallback)
309
+ if (val !== undefined) return val
310
+ }
311
+ return fallback
312
+ }
313
+
314
+ const isBoldActive = computed(() => {
315
+ return checkStyleVal('fontWeight') === 'bold'
316
+ })
317
+
318
+ const isItalicActive = computed(() => {
319
+ return checkStyleVal('fontStyle') === 'italic'
320
+ })
321
+
322
+ const isUnderlineActive = computed(() => {
323
+ return (checkStyleVal('textDecoration') || '').includes('underline')
324
+ })
325
+
326
+ const isStrikethroughActive = computed(() => {
327
+ return (checkStyleVal('textDecoration') || '').includes('line-through')
328
+ })
329
+
330
+ const currentTextAlign = computed(() => {
331
+ return props.styleTextAlign || checkStyleVal('textAlign', 'center') || 'center'
332
+ })
333
+
334
+ const currentFontVariants = computed(() => {
335
+ return getFontVariants(props.styleFontFamily)
336
+ })
337
+
338
+ const displaySysFonts = computed(() => {
339
+ return props.sysFonts || []
340
+ })
341
+
342
+ function toggleTextBorderHeader() {
343
+ if (!props.enableTextStroke) return
344
+ isTextBorderExpanded.value = !isTextBorderExpanded.value
345
+ }
346
+
347
+ function toggleTextShadowHeader() {
348
+ if (!props.enableTextShadow) return
349
+ isTextShadowExpanded.value = !isTextShadowExpanded.value
350
+ }
351
+
352
+ function toggleTextReflectionHeader() {
353
+ if (!props.enableTextReflection) return
354
+ isTextReflectionExpanded.value = !isTextReflectionExpanded.value
355
+ }
356
+
357
+ function startDragAngle(e: MouseEvent) {
358
+ const dial = e.currentTarget as HTMLElement
359
+ const rect = dial.getBoundingClientRect()
360
+ const centerX = rect.left + rect.width / 2
361
+ const centerY = rect.top + rect.height / 2
362
+
363
+ function updateAngle(moveEvent: MouseEvent) {
364
+ const dx = moveEvent.clientX - centerX
365
+ const dy = moveEvent.clientY - centerY
366
+ let angle = Math.round(Math.atan2(dy, dx) * 180 / Math.PI)
367
+ if (angle < 0) angle += 360
368
+ emit('update:textShadowAngle', angle)
369
+ }
370
+
371
+ updateAngle(e)
372
+
373
+ function stopDrag() {
374
+ window.removeEventListener('mousemove', updateAngle)
375
+ window.removeEventListener('mouseup', stopDrag)
376
+ }
377
+
378
+ window.addEventListener('mousemove', updateAngle)
379
+ window.addEventListener('mouseup', stopDrag)
380
+ }
381
+ </script>
382
+
383
+ <template>
384
+ <div class="sidebar-text-format-panel">
385
+ <!-- 1. 类型 (Text Classification Selector) -->
386
+ <div class="form-item-block">
387
+ <div class="form-item-label">类型</div>
388
+ <el-popover v-model:visible="isTextTypePopoverVisible" placement="bottom-start" :width="textTypeTriggerWidth"
389
+ trigger="click" popper-class="autofit-popover-panel" :show-arrow="false" :offset="4"
390
+ @before-enter="onTextTypePopoverShow">
391
+ <template #reference>
392
+ <div ref="textTypeTriggerRef" class="custom-select-trigger"
393
+ :class="{ 'is-active': isTextTypePopoverVisible }">
394
+ <span class="select-trigger-text">{{ currentTextTypeLabel }}</span>
395
+ <svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
396
+ :class="{ 'is-open': isTextTypePopoverVisible }">
397
+ <path
398
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
399
+ </path>
400
+ </svg>
401
+ </div>
402
+ </template>
403
+
404
+ <div class="autofit-menu-container">
405
+ <div v-for="item in TEXT_TYPE_LIST" :key="item.type" class="autofit-menu-item text-type-menu-item"
406
+ :class="{ 'is-selected': (currentTextType === item.type) || (!currentTextType && item.type === 'normalText') }"
407
+ @click="selectTextType(item.type)">
408
+ <div style="display: flex; align-items: center; gap: 8px;">
409
+ <span
410
+ :style="{ color: item.badgeColor, backgroundColor: item.badgeBg, fontSize: '10px', padding: '1px 6px', borderRadius: '4px', fontWeight: 600, minWidth: '36px', textAlign: 'center' }">{{
411
+ item.tag }}</span>
412
+ <span class="autofit-item-label">{{ item.label }}({{ item.enLabel }})</span>
413
+ </div>
414
+ <div style="display: flex; align-items: center; gap: 8px;">
415
+ <span style="font-size: 11px; color: #94a3b8;">{{ item.defaultFontSize }}px</span>
416
+ <svg v-if="(currentTextType === item.type) || (!currentTextType && item.type === 'normalText')"
417
+ viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5"
418
+ stroke-linecap="round" stroke-linejoin="round" class="autofit-check-icon">
419
+ <polyline points="20 6 9 17 4 12" />
420
+ </svg>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ </el-popover>
425
+ </div>
426
+
427
+ <!-- 2. 文字 (Font, Weight, Size, LineHeight, LetterSpacing, Lists, Alignments, Styles) -->
428
+ <div class="form-item-block">
429
+ <div class="form-item-label">文字</div>
430
+
431
+ <!-- Row 1: Font Family Selector (Full Width) -->
432
+ <el-select :model-value="styleFontFamily" @change="emit('changeFontFamily', $event)" size="small"
433
+ class="custom-select full-width-control" placement="bottom-start"
434
+ :fallback-placements="['bottom-start', 'bottom', 'top-start']" popper-class="sidebar-select-popper"
435
+ :show-arrow="false" :offset="4">
436
+ <el-option label="默认字体" value="默认字体" />
437
+ <el-option v-for="font in displaySysFonts" :key="font.value" :label="font.label" :value="font.value">
438
+ <span :style="{ fontFamily: font.value }">{{ font.label }}</span>
439
+ </el-option>
440
+ </el-select>
441
+
442
+ <!-- Row 2: Weight & Size (1:1 equal columns) -->
443
+ <div class="control-row-duo">
444
+ <div style="flex: 1; min-width: 0;">
445
+ <el-popover v-model:visible="isFontWeightPopoverVisible" placement="bottom-start"
446
+ :width="fontWeightTriggerWidth" trigger="click" popper-class="autofit-popover-panel" :show-arrow="false"
447
+ :offset="4" @before-enter="onFontWeightPopoverShow">
448
+ <template #reference>
449
+ <div ref="fontWeightTriggerRef" class="custom-select-trigger"
450
+ :class="{ 'is-active': isFontWeightPopoverVisible }">
451
+ <span class="select-trigger-text">{{ currentFontWeightLabel }}</span>
452
+ <svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
453
+ :class="{ 'is-open': isFontWeightPopoverVisible }">
454
+ <path
455
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
456
+ </path>
457
+ </svg>
458
+ </div>
459
+ </template>
460
+
461
+ <div class="autofit-menu-container">
462
+ <div v-for="variant in currentFontVariants" :key="variant.value" class="autofit-menu-item"
463
+ :class="{ 'is-selected': styleFontWeightStyle === variant.value || (!styleFontWeightStyle && variant.value === 'normal') }"
464
+ @click="selectFontWeight(variant.value)">
465
+ <span class="autofit-item-label">{{ variant.label }}</span>
466
+ <svg
467
+ v-if="styleFontWeightStyle === variant.value || (!styleFontWeightStyle && variant.value === 'normal')"
468
+ viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5"
469
+ stroke-linecap="round" stroke-linejoin="round" class="autofit-check-icon">
470
+ <polyline points="20 6 9 17 4 12" />
471
+ </svg>
472
+ </div>
473
+ </div>
474
+ </el-popover>
475
+ </div>
476
+
477
+ <div style="flex: 1; min-width: 0;">
478
+ <el-popover v-model:visible="isFontSizePopoverVisible" placement="bottom-end" :width="fontSizeTriggerWidth"
479
+ trigger="click" popper-class="autofit-popover-panel font-size-popover" :show-arrow="false" :offset="4"
480
+ @before-enter="onFontSizePopoverShow">
481
+ <template #reference>
482
+ <div ref="fontSizeTriggerRef" class="custom-select-trigger font-size-trigger"
483
+ :class="{ 'is-active': isFontSizePopoverVisible }">
484
+ <input type="text" :value="fontSizeClean" class="custom-select-input" placeholder="字号"
485
+ @input="onFontSizeInput" @keydown.enter="onFontSizeEnter" @blur="onFontSizeBlur" />
486
+ <svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
487
+ :class="{ 'is-open': isFontSizePopoverVisible }">
488
+ <path
489
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
490
+ </path>
491
+ </svg>
492
+ </div>
493
+ </template>
494
+
495
+ <el-scrollbar max-height="220px">
496
+ <div class="autofit-menu-container font-size-menu-container">
497
+ <div v-for="size in ['12', '14', '16', '18', '20', '24', '28', '32', '36', '44', '52', '60', '72']"
498
+ :key="size" class="autofit-menu-item font-size-item"
499
+ :class="{ 'is-selected': fontSizeClean === size }" @click="selectFontSize(size)">
500
+ <span class="autofit-item-label">{{ size }}</span>
501
+ <svg v-if="fontSizeClean === size" viewBox="0 0 24 24" width="14" height="14" fill="none"
502
+ stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
503
+ class="autofit-check-icon">
504
+ <polyline points="20 6 9 17 4 12" />
505
+ </svg>
506
+ </div>
507
+ </div>
508
+ </el-scrollbar>
509
+ </el-popover>
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Row 3: Line Height & Letter Spacing (1:1 equal columns) -->
514
+ <div class="control-row-duo">
515
+ <div class="custom-num-pill" style="flex: 1;">
516
+ <svg class="icon input-icon">
517
+ <use xlink:href="#icon-hanggao"></use>
518
+ </svg>
519
+ <input type="number" :value="lineHeightNumber"
520
+ @input="emit('update:lineHeightNumber', Number(($event.target as HTMLInputElement).value))"
521
+ class="custom-num-field" step="0.1" min="0.5" max="50" />
522
+ </div>
523
+ <div class="custom-num-pill" style="flex: 1;">
524
+ <svg class="icon input-icon">
525
+ <use xlink:href="#icon-zijianju"></use>
526
+ </svg>
527
+ <input type="number" :value="letterSpacingNumber"
528
+ @input="emit('update:letterSpacingNumber', Number(($event.target as HTMLInputElement).value))"
529
+ class="custom-num-field" min="-10" max="50" />
530
+ </div>
531
+ </div>
532
+
533
+ <!-- Row 4: Lists & Vertical Alignment (merged in one row) -->
534
+ <div class="control-row-trio">
535
+ <!-- Unordered List Dropdown Button -->
536
+ <div class="list-dropdown-btn" :class="{ 'is-active': hasUnorderedList }">
537
+ <div class="list-btn-main" @click="emit('toggleListStyle', 'ul', 'disc')" @mousedown.prevent title="无序列表">
538
+ <svg class="icon" style="width: 14px; height: 14px; fill: currentColor;">
539
+ <use xlink:href="#icon-wuxuliebiao"></use>
540
+ </svg>
541
+ </div>
542
+ <el-popover placement="bottom" :width="250" trigger="click" popper-class="list-popover-panel"
543
+ :show-arrow="false" @before-enter="isUlVisible = true" @before-leave="isUlVisible = false">
544
+ <template #reference>
545
+ <div class="list-btn-arrow" :class="{ 'is-open': isUlVisible }">
546
+ <svg viewBox="0 0 1024 1024" width="7" height="7" fill="currentColor">
547
+ <path
548
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
549
+ </path>
550
+ </svg>
551
+ </div>
552
+ </template>
553
+ <div class="list-presets-grid">
554
+ <div class="list-style-card"
555
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ul', 'disc') : false }"
556
+ @click="emit('toggleListStyle', 'ul', 'disc', true)" @mousedown.prevent>
557
+ <img :src="ulDisc" />
558
+ </div>
559
+ <div class="list-style-card"
560
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ul', '\'◆ \'') : false }"
561
+ @click="emit('toggleListStyle', 'ul', '\'◆ \'', true)" @mousedown.prevent>
562
+ <img :src="ulDiamond" />
563
+ </div>
564
+ <div class="list-style-card"
565
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ul', '\'▸ \'') : false }"
566
+ @click="emit('toggleListStyle', 'ul', '\'▸ \'', true)" @mousedown.prevent>
567
+ <img :src="ulTriangle" />
568
+ </div>
569
+ <div class="list-style-card"
570
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ul', '\'✔ \'') : false }"
571
+ @click="emit('toggleListStyle', 'ul', '\'✔ \'', true)" @mousedown.prevent>
572
+ <img :src="ulCheckmark" />
573
+ </div>
574
+ <div class="list-style-card"
575
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ul', 'square') : false }"
576
+ @click="emit('toggleListStyle', 'ul', 'square', true)" @mousedown.prevent>
577
+ <img :src="ulSquare" />
578
+ </div>
579
+ <div class="list-style-card"
580
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ul', 'circle') : false }"
581
+ @click="emit('toggleListStyle', 'ul', 'circle', true)" @mousedown.prevent>
582
+ <img :src="ulCircle" />
583
+ </div>
584
+ </div>
585
+ </el-popover>
586
+ </div>
587
+
588
+ <!-- Ordered List Dropdown Button -->
589
+ <div class="list-dropdown-btn" :class="{ 'is-active': hasOrderedList }">
590
+ <div class="list-btn-main" @click="emit('toggleListStyle', 'ol', 'decimal')" @mousedown.prevent title="有序列表">
591
+ <svg class="icon" style="width: 14px; height: 14px; fill: currentColor;">
592
+ <use xlink:href="#icon-youxuliebiao"></use>
593
+ </svg>
594
+ </div>
595
+ <el-popover placement="bottom" :width="250" trigger="click" popper-class="list-popover-panel"
596
+ :show-arrow="false" @before-enter="isOlVisible = true" @before-leave="isOlVisible = false">
597
+ <template #reference>
598
+ <div class="list-btn-arrow" :class="{ 'is-open': isOlVisible }">
599
+ <svg viewBox="0 0 1024 1024" width="7" height="7" fill="currentColor">
600
+ <path
601
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
602
+ </path>
603
+ </svg>
604
+ </div>
605
+ </template>
606
+ <div class="list-presets-grid">
607
+ <div class="list-style-card"
608
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'decimal') : false }"
609
+ @click="emit('toggleListStyle', 'ol', 'decimal', true)" @mousedown.prevent>
610
+ <img :src="olDicimal" />
611
+ </div>
612
+ <div class="list-style-card"
613
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'circled-numbers') : false }"
614
+ @click="emit('toggleListStyle', 'ol', 'circled-numbers', true)" @mousedown.prevent>
615
+ <img :src="olCircledNumbers" />
616
+ </div>
617
+ <div class="list-style-card"
618
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'lower-alpha') : false }"
619
+ @click="emit('toggleListStyle', 'ol', 'lower-alpha', true)" @mousedown.prevent>
620
+ <img :src="olLowerAlpha" />
621
+ </div>
622
+ <div class="list-style-card"
623
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'upper-alpha') : false }"
624
+ @click="emit('toggleListStyle', 'ol', 'upper-alpha', true)" @mousedown.prevent>
625
+ <img :src="olUpperAlpha" />
626
+ </div>
627
+ <div class="list-style-card"
628
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'upper-roman') : false }"
629
+ @click="emit('toggleListStyle', 'ol', 'upper-roman', true)" @mousedown.prevent>
630
+ <img :src="olUpperRoman" />
631
+ </div>
632
+ <div class="list-style-card"
633
+ :class="{ active: isActiveListStyle ? isActiveListStyle('ol', 'simp-chinese-informal') : false }"
634
+ @click="emit('toggleListStyle', 'ol', 'simp-chinese-informal', true)" @mousedown.prevent>
635
+ <img :src="olChinese" />
636
+ </div>
637
+ </div>
638
+ </el-popover>
639
+ </div>
640
+
641
+ <!-- Vertical Alignment 3-Button Group -->
642
+ <div class="button-group-pill vertical-align-group">
643
+ <div class="pill-action-btn" :class="{ 'is-active': styleVerticalAlign === 'top' }" title="顶对齐"
644
+ @click="emit('update:styleVerticalAlign', 'top')">
645
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
646
+ <use :xlink:href="styleVerticalAlign === 'top' ? '#icon-dingduiqi-xuanzhong' : '#icon-dingduiqi1'"></use>
647
+ </svg>
648
+ </div>
649
+ <div class="pill-action-btn"
650
+ :class="{ 'is-active': (styleVerticalAlign === 'middle' || !styleVerticalAlign) }" title="居中对齐"
651
+ @click="emit('update:styleVerticalAlign', 'middle')">
652
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
653
+ <use
654
+ :xlink:href="(styleVerticalAlign === 'middle' || !styleVerticalAlign) ? '#icon-chuizhiduiqi-xuanzhong' : '#icon-chuizhiduiqi'">
655
+ </use>
656
+ </svg>
657
+ </div>
658
+ <div class="pill-action-btn" :class="{ 'is-active': styleVerticalAlign === 'bottom' }" title="底对齐"
659
+ @click="emit('update:styleVerticalAlign', 'bottom')">
660
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
661
+ <use :xlink:href="styleVerticalAlign === 'bottom' ? '#icon-diduiqi1-copy' : '#icon-diduiqi1'"></use>
662
+ </svg>
663
+ </div>
664
+ </div>
665
+ </div>
666
+
667
+ <!-- Row 5: Text Styles: Bold, Italic, Underline, Strikethrough -->
668
+ <div class="button-group-pill full-width-control">
669
+ <div class="pill-action-btn font-style-letter" :class="{ 'is-active': isBoldActive }" title="加粗"
670
+ @click="emit('toggleBold')">
671
+ <span style="font-weight: 700; font-size: 14px;">B</span>
672
+ </div>
673
+ <div class="pill-action-btn font-style-letter" :class="{ 'is-active': isItalicActive }" title="斜体"
674
+ @click="emit('toggleItalic')">
675
+ <span
676
+ style="font-style: italic; font-weight: 600; font-size: 14px; font-family: serif, 'Times New Roman';">I</span>
677
+ </div>
678
+ <div class="pill-action-btn font-style-letter" :class="{ 'is-active': isUnderlineActive }" title="下划线"
679
+ @click="emit('toggleUnderline')">
680
+ <span style="text-decoration: underline; font-weight: 600; font-size: 14px;">U</span>
681
+ </div>
682
+ <div class="pill-action-btn" :class="{ 'is-active': isStrikethroughActive }" title="删除线"
683
+ @click="emit('toggleStrikethrough')">
684
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
685
+ <use :xlink:href="isStrikethroughActive ? '#icon-shanchuxian-xuanzhong' : '#icon-shanchuxian'"></use>
686
+ </svg>
687
+ </div>
688
+ </div>
689
+
690
+ <!-- Row 6: Horizontal Alignment (5 buttons) -->
691
+ <div class="button-group-pill full-width-control">
692
+ <div class="pill-action-btn" :class="{ 'is-active': currentTextAlign === 'left' }" title="左对齐"
693
+ @click="emit('update:styleTextAlign', 'left')">
694
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
695
+ <use :xlink:href="currentTextAlign === 'left' ? '#icon-zuoduiqi-xuanzhong' : '#icon-zuoduiqi1'"></use>
696
+ </svg>
697
+ </div>
698
+ <div class="pill-action-btn" :class="{ 'is-active': (currentTextAlign === 'center' || !currentTextAlign) }"
699
+ title="居中对齐" @click="emit('update:styleTextAlign', 'center')">
700
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
701
+ <use
702
+ :xlink:href="(currentTextAlign === 'center' || !currentTextAlign) ? '#icon-juzhongduiqi-xuanzhong' : '#icon-juzhongduiqi'">
703
+ </use>
704
+ </svg>
705
+ </div>
706
+ <div class="pill-action-btn" :class="{ 'is-active': currentTextAlign === 'right' }" title="右对齐"
707
+ @click="emit('update:styleTextAlign', 'right')">
708
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
709
+ <use :xlink:href="currentTextAlign === 'right' ? '#icon-youduiqi3-copy-copy' : '#icon-youduiqi3-copy'">
710
+ </use>
711
+ </svg>
712
+ </div>
713
+ <div class="pill-action-btn" :class="{ 'is-active': currentTextAlign === 'justify' }" title="两端对齐"
714
+ @click="emit('update:styleTextAlign', 'justify')">
715
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
716
+ <use xlink:href="#icon-liangduanduiqi"></use>
717
+ </svg>
718
+ </div>
719
+ <div class="pill-action-btn" :class="{ 'is-active': currentTextAlign === 'distribute' }" title="分散对齐"
720
+ @click="emit('update:styleTextAlign', 'distribute')">
721
+ <svg class="icon" style="width: 15px; height: 15px; fill: currentColor;">
722
+ <use xlink:href="#icon-fensanduiqi"></use>
723
+ </svg>
724
+ </div>
725
+ </div>
726
+ </div>
727
+
728
+ <div class="section-divider"></div>
729
+
730
+ <!-- 3. 字体颜色 -->
731
+ <div class="form-item-block">
732
+ <div class="form-item-label">字体颜色</div>
733
+ <QuickColorPopover v-model:visible="textColorPopoverVisible" :color="styleTextColor || '#000000'"
734
+ :recent-colors="recentColors" @select-color="emit('update:styleTextColor', $event)">
735
+ <template #reference>
736
+ <div class="full-color-card">
737
+ <div class="color-swatch-hex">
738
+ <div class="color-swatch-box" :style="{ background: styleTextColor || '#000000' }"></div>
739
+ <span class="color-hex-text">{{ (styleTextColor || '#000000').replace('#', '').toUpperCase() }}</span>
740
+ </div>
741
+ <span class="color-opacity-text">100%</span>
742
+ </div>
743
+ </template>
744
+ </QuickColorPopover>
745
+ </div>
746
+
747
+ <!-- 4. 高亮色 -->
748
+ <div class="form-item-block">
749
+ <div class="form-item-label">高亮色</div>
750
+ <QuickColorPopover v-model:visible="textBgPopoverVisible" :color="styleTextBackground || 'transparent'"
751
+ :recent-colors="recentColors" @select-color="emit('update:styleTextBackground', $event)">
752
+ <template #reference>
753
+ <div class="full-color-card">
754
+ <div class="color-swatch-hex">
755
+ <div class="color-swatch-box" :style="{
756
+ background: (styleTextBackground && styleTextBackground !== 'transparent') ? styleTextBackground : '#ffffff',
757
+ border: (!styleTextBackground || styleTextBackground === 'transparent') ? '1px dashed #cbd5e1' : 'none'
758
+ }"></div>
759
+ <span class="color-hex-text">{{ (!styleTextBackground || styleTextBackground === 'transparent') ? '无' :
760
+ styleTextBackground.replace('#', '').toUpperCase() }}</span>
761
+ </div>
762
+ <span class="color-opacity-text">{{ (!styleTextBackground || styleTextBackground === 'transparent') ? '0%' :
763
+ '100%' }}</span>
764
+ </div>
765
+ </template>
766
+ </QuickColorPopover>
767
+ </div>
768
+
769
+ <!-- 5. 文字轮廓 -->
770
+ <div class="accordion-item-block">
771
+ <div class="accordion-header-row" @click="toggleTextBorderHeader">
772
+ <div class="accordion-title-wrap">
773
+ <span class="accordion-title">文字轮廓</span>
774
+ <span class="accordion-arrow"
775
+ :style="{ transform: (enableTextStroke && isTextBorderExpanded) ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>
776
+ </div>
777
+ <el-switch :model-value="enableTextStroke" size="small" @click.stop
778
+ @change="emit('update:enableTextStroke', $event); isTextBorderExpanded = $event" />
779
+ </div>
780
+
781
+ <div v-show="isTextBorderExpanded && enableTextStroke" class="accordion-inner-content">
782
+ <!-- Stroke Color card -->
783
+ <QuickColorPopover v-model:visible="textStrokePopoverVisible" :color="textStrokeColor || '#000000'"
784
+ :recent-colors="recentColors" @select-color="emit('update:textStrokeColor', $event)">
785
+ <template #reference>
786
+ <div class="full-color-card">
787
+ <div class="color-swatch-hex">
788
+ <div class="color-swatch-box" :style="{ background: textStrokeColor || '#000000' }"></div>
789
+ <span class="color-hex-text">{{ (textStrokeColor || '#000000').replace('#', '').toUpperCase() }}</span>
790
+ </div>
791
+ <span class="color-opacity-text">{{ textStrokeAlpha ?? 100 }}%</span>
792
+ </div>
793
+ </template>
794
+ </QuickColorPopover>
795
+
796
+ <!-- Stroke Width Pill -->
797
+ <div class="custom-num-pill full-width-pill">
798
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5"
799
+ class="input-icon">
800
+ <line x1="3" y1="6" x2="21" y2="6" />
801
+ <line x1="3" y1="12" x2="21" y2="12" />
802
+ <line x1="3" y1="18" x2="21" y2="18" />
803
+ </svg>
804
+ <input type="number" :value="textStrokeWidth"
805
+ @input="emit('update:textStrokeWidth', Number(($event.target as HTMLInputElement).value))" :min="1"
806
+ :max="20" class="custom-num-field" />
807
+ </div>
808
+ </div>
809
+ </div>
810
+
811
+ <div class="section-divider"></div>
812
+
813
+ <!-- 6. 文字边距 -->
814
+ <div class="accordion-item-block" v-if="selectedElement && selectedElement.type !== 'group'">
815
+ <div class="accordion-header-row" @click="isPaddingExpanded = !isPaddingExpanded">
816
+ <div class="accordion-title-wrap">
817
+ <span class="accordion-title">文字边距</span>
818
+ <span class="accordion-arrow"
819
+ :style="{ transform: isPaddingExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>
820
+ </div>
821
+ </div>
822
+
823
+ <div v-show="isPaddingExpanded" class="accordion-inner-content">
824
+ <div class="padding-control-container">
825
+ <!-- 2x2 grid -->
826
+ <div class="padding-inputs-grid">
827
+ <!-- Left (Top-Left) -->
828
+ <div class="custom-num-pill">
829
+ <svg class="icon input-icon">
830
+ <use xlink:href="#icon-zuobianju"></use>
831
+ </svg>
832
+ <input type="number" :value="paddingLeft"
833
+ @input="emit('update:paddingLeft', Number(($event.target as HTMLInputElement).value))"
834
+ class="custom-num-field" min="0" max="500" />
835
+ </div>
836
+
837
+ <!-- Right (Top-Right) -->
838
+ <div class="custom-num-pill">
839
+ <svg class="icon input-icon">
840
+ <use xlink:href="#icon-youbianju"></use>
841
+ </svg>
842
+ <input type="number" :value="paddingRight"
843
+ @input="emit('update:paddingRight', Number(($event.target as HTMLInputElement).value))"
844
+ class="custom-num-field" min="0" max="500" />
845
+ </div>
846
+
847
+ <!-- Top (Bottom-Left) -->
848
+ <div class="custom-num-pill">
849
+ <svg class="icon input-icon">
850
+ <use xlink:href="#icon-shangbianju"></use>
851
+ </svg>
852
+ <input type="number" :value="paddingTop"
853
+ @input="emit('update:paddingTop', Number(($event.target as HTMLInputElement).value))"
854
+ class="custom-num-field" min="0" max="500" />
855
+ </div>
856
+
857
+ <!-- Bottom (Bottom-Right) -->
858
+ <div class="custom-num-pill">
859
+ <svg class="icon input-icon">
860
+ <use xlink:href="#icon-xiabianju"></use>
861
+ </svg>
862
+ <input type="number" :value="paddingBottom"
863
+ @input="emit('update:paddingBottom', Number(($event.target as HTMLInputElement).value))"
864
+ class="custom-num-field" min="0" max="500" />
865
+ </div>
866
+ </div>
867
+
868
+ <!-- Lock Button on bottom-right -->
869
+ <div class="padding-lock-btn" title="锁定所有边框内边距一致" :class="{ 'is-locked': isPaddingLocked }"
870
+ @click="emit('update:isPaddingLocked', !isPaddingLocked)">
871
+ <svg viewBox="0 0 1024 1024" width="16" height="16" style="display: block;">
872
+ <template v-if="isPaddingLocked">
873
+ <path
874
+ d="M192 396.8v-30.4q0-125.248 88.576-213.824Q369.152 64 494.4 64q125.248 0 213.824 88.576 88.576 88.576 88.576 213.824V396.8a46.4 46.4 0 1 1-92.8 0v-30.4q0-86.848-61.44-148.224-61.312-61.376-148.16-61.376t-148.224 61.44q-61.376 61.312-61.376 148.16V396.8A46.4 46.4 0 1 1 192 396.8m92.8 259.2v30.4q0 86.848 61.44 148.224 61.312 61.376 148.16 61.376t148.224-61.44q61.376-61.312 61.376-148.16v-30.4a46.4 46.4 0 1 1 92.8 0v30.4q0 125.248-88.576 213.824-88.576 88.576-213.824 88.576-125.248 0-213.824-88.576Q192 811.648 192 686.4v-30.4a46.4 46.4 0 1 1 92.8 0"
875
+ fill="currentColor" />
876
+ <path
877
+ d="M448 320m46.4 0l0 0q46.4 0 46.4 46.4l0 320q0 46.4-46.4 46.4l0 0q-46.4 0-46.4-46.4l0-320q0-46.4 46.4-46.4Z"
878
+ fill="currentColor" />
879
+ </template>
880
+ <template v-else>
881
+ <path
882
+ d="M192 396.8v-30.4q0-125.248 88.576-213.824Q369.152 64 494.4 64q125.248 0 213.824 88.576 88.576 88.576 88.576 213.824V396.8a46.4 46.4 0 1 1-92.8 0v-30.4q0-86.848-61.44-148.224-61.312-61.376-148.16-61.376t-148.224 61.44q-61.376 61.312-61.376 148.16V396.8A46.4 46.4 0 1 1 192 396.8m92.8 259.2v30.4q0 86.848 61.44 148.224 61.312 61.376 148.16 61.376t148.224-61.44q61.376-61.312 61.376-148.16v-30.4a46.4 46.4 0 1 1 92.8 0v30.4q0 125.248-88.576 213.824-88.576 88.576-213.824 88.576-125.248 0-213.824-88.576Q192 811.648 192 686.4v-30.4a46.4 46.4 0 1 1 92.8 0"
883
+ fill="currentColor" />
884
+ <path
885
+ d="M448 604.8v81.6q0 4.544 0.896 9.024 0.896 4.48 2.624 8.704 1.792 4.224 4.288 8.064 2.56 3.84 5.76 7.04 3.2 3.2 7.04 5.76 3.84 2.56 8.064 4.288 4.224 1.728 8.704 2.624 4.48 0.896 9.024 0.896 4.544 0 9.024-0.896 4.48-0.896 8.704-2.624 4.224-1.792 8.064-4.288 3.84-2.56 7.04-5.76 3.2-3.2 5.76-7.04 2.56-3.84 4.288-8.064 1.728-4.224 2.624-8.704 0.896-4.48 0.896-9.024V604.8H448zM448 512h92.8V366.4q0-4.544-0.896-9.024-0.896-4.48-2.624-8.704-1.792-4.224-4.288-8.064-2.56-3.84-5.76-7.04-3.2-3.2-7.04-5.76-3.84-2.56-8.064-4.288-4.224-1.728-8.704-2.624Q498.944 320 494.4 320q-4.544 0-9.024 0.896-4.48 0.896-8.704 2.624-4.224 1.792-8.064 4.288-3.84 2.56-7.04 5.76-3.2 3.2-5.76 7.04-2.56 3.84-4.288 8.064-1.728 4.224-2.624 8.704Q448 361.856 448 366.4V512z"
886
+ fill="currentColor" />
887
+ <path d="M142.4 480a46.4 46.4 0 1 0 0 92.8h675.2a46.4 46.4 0 1 0 0-92.8H142.4z" fill="currentColor" />
888
+ </template>
889
+ </svg>
890
+ </div>
891
+ </div>
892
+ </div>
893
+ </div>
894
+
895
+ <div class="section-divider"></div>
896
+
897
+ <!-- 7. 文字方向 (Custom Popover matching AutoFit style) -->
898
+ <div class="form-item-block">
899
+ <div class="form-item-label">文字方向</div>
900
+ <el-popover v-model:visible="isWritingModePopoverVisible" placement="bottom-start"
901
+ :width="writingModeTriggerWidth" trigger="click" popper-class="autofit-popover-panel" :show-arrow="false"
902
+ :offset="4" @before-enter="onWritingModePopoverShow">
903
+ <template #reference>
904
+ <div ref="writingModeTriggerRef" class="custom-select-trigger"
905
+ :class="{ 'is-active': isWritingModePopoverVisible }">
906
+ <span class="select-trigger-text">{{ currentWritingModeLabel }}</span>
907
+ <svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
908
+ :class="{ 'is-open': isWritingModePopoverVisible }">
909
+ <path
910
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
911
+ </path>
912
+ </svg>
913
+ </div>
914
+ </template>
915
+
916
+ <div class="autofit-menu-container">
917
+ <div v-for="item in writingModeOptions" :key="item.value" class="autofit-menu-item"
918
+ :class="{ 'is-selected': (styleWritingMode === item.value) || (!styleWritingMode && item.value === 'horizontal-tb') }"
919
+ @click="selectWritingMode(item.value)">
920
+ <span class="autofit-item-label">{{ item.label }}</span>
921
+ <svg v-if="(styleWritingMode === item.value) || (!styleWritingMode && item.value === 'horizontal-tb')"
922
+ viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5"
923
+ class="autofit-check-icon">
924
+ <polyline points="20 6 9 17 4 12"></polyline>
925
+ </svg>
926
+ </div>
927
+ </div>
928
+ </el-popover>
929
+ </div>
930
+
931
+ <!-- 8. 文字自动调整 (Custom Popover with Word Wrap Toggle) -->
932
+ <div class="form-item-block">
933
+ <div class="form-item-label">文字自动调整</div>
934
+ <el-popover v-model:visible="isAutoFitPopoverVisible" placement="bottom-start" :width="autoFitTriggerWidth"
935
+ trigger="click" popper-class="autofit-popover-panel" :show-arrow="false" :offset="4"
936
+ @before-enter="onAutoFitPopoverShow">
937
+ <template #reference>
938
+ <div ref="autoFitTriggerRef" class="custom-select-trigger" :class="{ 'is-active': isAutoFitPopoverVisible }">
939
+ <span class="select-trigger-text">{{ currentAutoFitLabel }}</span>
940
+ <svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
941
+ :class="{ 'is-open': isAutoFitPopoverVisible }">
942
+ <path
943
+ d="M831.872 340.777L512 652.672 192.128 340.777a31.968 31.968 0 0 0-45.12 45.312l342.4 333.568a32 32 0 0 0 45.184 0l342.4-333.568a31.968 31.968 0 0 0-45.12-45.312z">
944
+ </path>
945
+ </svg>
946
+ </div>
947
+ </template>
948
+
949
+ <div class="autofit-menu-container">
950
+ <div v-for="item in autoFitOptions" :key="item.value" class="autofit-menu-item"
951
+ :class="{ 'is-selected': (styleAutoFitType === item.value) || (!styleAutoFitType && item.value === 'none') }"
952
+ @click="selectAutoFitType(item.value)">
953
+ <span class="autofit-item-label">{{ item.label }}</span>
954
+ <svg v-if="(styleAutoFitType === item.value) || (!styleAutoFitType && item.value === 'none')"
955
+ viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.5"
956
+ class="autofit-check-icon">
957
+ <polyline points="20 6 9 17 4 12"></polyline>
958
+ </svg>
959
+ </div>
960
+
961
+ <div class="autofit-divider"></div>
962
+
963
+ <div class="autofit-wrap-row" @click.stop>
964
+ <span class="autofit-wrap-label">形状中的文字自动换行</span>
965
+ <el-switch :model-value="styleWordWrap" @change="emit('update:styleWordWrap', $event)" size="small" />
966
+ </div>
967
+ </div>
968
+ </el-popover>
969
+ </div>
970
+
971
+ <!-- 10. 阴影 (Shadow Accordion) -->
972
+ <div class="accordion-item-block">
973
+ <div class="accordion-header-row" @click="toggleTextShadowHeader">
974
+ <div class="accordion-title-wrap">
975
+ <span class="accordion-title">阴影</span>
976
+ <span class="accordion-arrow"
977
+ :style="{ transform: (enableTextShadow && isTextShadowExpanded) ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>
978
+ </div>
979
+ <el-switch :model-value="enableTextShadow" size="small" @click.stop
980
+ @change="emit('update:enableTextShadow', $event); isTextShadowExpanded = $event" />
981
+ </div>
982
+
983
+ <div v-show="isTextShadowExpanded && enableTextShadow" class="accordion-inner-content">
984
+ <!-- Shadow color -->
985
+ <QuickColorPopover v-model:visible="textShadowPopoverVisible" :color="textShadowHex || '#000000'"
986
+ :recent-colors="recentColors" @select-color="emit('update:textShadowHex', $event)">
987
+ <template #reference>
988
+ <div class="full-color-card">
989
+ <div class="color-swatch-hex">
990
+ <div class="color-swatch-box" :style="{ background: textShadowHex || '#000000' }"></div>
991
+ <span class="color-hex-text">{{ (textShadowHex || '#000000').replace('#', '').toUpperCase() }}</span>
992
+ </div>
993
+ <span class="color-opacity-text">{{ textShadowAlpha ?? 100 }}%</span>
994
+ </div>
995
+ </template>
996
+ </QuickColorPopover>
997
+
998
+ <!-- Angle -->
999
+ <div class="shape-property-row">
1000
+ <span class="slider-prop-label">角度</span>
1001
+ <div style="display: flex; align-items: center; gap: 8px; flex: 1; justify-content: flex-end;">
1002
+ <div class="custom-num-pill" style="width: 80px; position: relative;">
1003
+ <input type="number" :value="textShadowAngle"
1004
+ @input="emit('update:textShadowAngle', Number(($event.target as HTMLInputElement).value))"
1005
+ class="custom-num-field" />
1006
+ <span class="angle-icon"
1007
+ :style="{ left: `calc(8px + ${String(textShadowAngle ?? 0).length * 8}px)` }">°</span>
1008
+ </div>
1009
+ <div class="angle-dial" @mousedown="startDragAngle">
1010
+ <div :style="{ transform: `rotate(${textShadowAngle || 0}deg)` }" class="dial-needle">
1011
+ <div class="dial-needle-tip"></div>
1012
+ </div>
1013
+ <div class="dial-center-dot"></div>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <!-- Size -->
1019
+ <div class="shape-property-row">
1020
+ <span class="slider-prop-label">大小</span>
1021
+ <div class="shape-slider-wrapper">
1022
+ <el-slider :model-value="textShadowSize" @update:model-value="emit('update:textShadowSize', $event)"
1023
+ :min="0" :max="200" :step="1" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false"
1024
+ show-input :show-input-controls="false" />
1025
+ </div>
1026
+ </div>
1027
+
1028
+ <!-- Blur -->
1029
+ <div class="shape-property-row">
1030
+ <span class="slider-prop-label">模糊</span>
1031
+ <div class="shape-slider-wrapper">
1032
+ <el-slider :model-value="textShadowBlur" @update:model-value="emit('update:textShadowBlur', $event)"
1033
+ :min="0" :max="267" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1034
+ :show-input-controls="false" />
1035
+ </div>
1036
+ </div>
1037
+
1038
+ <!-- Distance -->
1039
+ <div class="shape-property-row">
1040
+ <span class="slider-prop-label">距离</span>
1041
+ <div class="shape-slider-wrapper">
1042
+ <el-slider :model-value="textShadowDistance" @update:model-value="emit('update:textShadowDistance', $event)"
1043
+ :min="0" :max="267" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1044
+ :show-input-controls="false" />
1045
+ </div>
1046
+ </div>
1047
+
1048
+ <!-- Transparency -->
1049
+ <div class="shape-property-row">
1050
+ <span class="slider-prop-label">透明度</span>
1051
+ <div class="shape-slider-wrapper">
1052
+ <el-slider :model-value="textShadowAlpha" @update:model-value="emit('update:textShadowAlpha', $event)"
1053
+ :min="0" :max="100" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1054
+ :show-input-controls="false" />
1055
+ </div>
1056
+ </div>
1057
+ </div>
1058
+ </div>
1059
+
1060
+ <!-- 11. 倒影 (Reflection Accordion) -->
1061
+ <div class="accordion-item-block">
1062
+ <div class="accordion-header-row" @click="toggleTextReflectionHeader">
1063
+ <div class="accordion-title-wrap">
1064
+ <span class="accordion-title">倒影</span>
1065
+ <span class="accordion-arrow"
1066
+ :style="{ transform: (enableTextReflection && isTextReflectionExpanded) ? 'rotate(90deg)' : 'rotate(0deg)' }">▶</span>
1067
+ </div>
1068
+ <el-switch :model-value="enableTextReflection" size="small" @click.stop
1069
+ @change="emit('update:enableTextReflection', $event); isTextReflectionExpanded = $event" />
1070
+ </div>
1071
+
1072
+ <div v-show="isTextReflectionExpanded && enableTextReflection" class="accordion-inner-content">
1073
+ <!-- Distance -->
1074
+ <div class="shape-property-row">
1075
+ <span class="slider-prop-label">距离</span>
1076
+ <div class="shape-slider-wrapper">
1077
+ <el-slider :model-value="textReflectionDistance"
1078
+ @update:model-value="emit('update:textReflectionDistance', $event)" :min="0" :max="100" :step="0.1"
1079
+ size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1080
+ :show-input-controls="false" />
1081
+ </div>
1082
+ </div>
1083
+
1084
+ <!-- Size -->
1085
+ <div class="shape-property-row">
1086
+ <span class="slider-prop-label">大小</span>
1087
+ <div class="shape-slider-wrapper">
1088
+ <el-slider :model-value="textReflectionSize" @update:model-value="emit('update:textReflectionSize', $event)"
1089
+ :min="0" :max="100" :step="0.1" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false"
1090
+ show-input :show-input-controls="false" />
1091
+ </div>
1092
+ </div>
1093
+
1094
+ <!-- Opacity -->
1095
+ <div class="shape-property-row">
1096
+ <span class="slider-prop-label">透明度</span>
1097
+ <div class="shape-slider-wrapper">
1098
+ <el-slider :model-value="textReflectionOpacity"
1099
+ @update:model-value="emit('update:textReflectionOpacity', $event)" :min="0" :max="100" :step="1"
1100
+ size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false" show-input
1101
+ :show-input-controls="false" />
1102
+ </div>
1103
+ </div>
1104
+
1105
+ <!-- Blur -->
1106
+ <div class="shape-property-row">
1107
+ <span class="slider-prop-label">模糊</span>
1108
+ <div class="shape-slider-wrapper">
1109
+ <el-slider :model-value="textReflectionBlur" @update:model-value="emit('update:textReflectionBlur', $event)"
1110
+ :min="0" :max="100" :step="0.1" size="small" style="flex: 1;" class="custom-slider" :show-tooltip="false"
1111
+ show-input :show-input-controls="false" />
1112
+ </div>
1113
+ </div>
1114
+ </div>
1115
+ </div>
1116
+ </div>
1117
+ </template>
1118
+
1119
+ <style scoped lang="scss">
1120
+ .sidebar-text-format-panel {
1121
+ padding: 16px 16px 40px;
1122
+ display: flex;
1123
+ flex-direction: column;
1124
+ gap: 14px;
1125
+ box-sizing: border-box;
1126
+ }
1127
+
1128
+ .form-item-block {
1129
+ display: flex;
1130
+ flex-direction: column;
1131
+ gap: 8px;
1132
+ }
1133
+
1134
+ .form-item-label {
1135
+ font-size: 13px;
1136
+ font-weight: 500;
1137
+ color: #1e293b;
1138
+ text-align: left;
1139
+ user-select: none;
1140
+ }
1141
+
1142
+ .full-width-control {
1143
+ width: 100% !important;
1144
+ }
1145
+
1146
+ .control-row-duo {
1147
+ display: flex;
1148
+ align-items: center;
1149
+ gap: 8px;
1150
+ width: 100%;
1151
+ }
1152
+
1153
+ .control-row-trio {
1154
+ display: flex;
1155
+ align-items: center;
1156
+ gap: 8px;
1157
+ width: 100%;
1158
+ }
1159
+
1160
+ .custom-num-pill {
1161
+ display: flex;
1162
+ align-items: center;
1163
+ background: #FAFAFA;
1164
+ border: 1px solid #edf0f3;
1165
+ border-radius: 8px;
1166
+ height: 32px;
1167
+ padding: 0 10px;
1168
+ box-sizing: border-box;
1169
+ transition: border-color 0.15s ease;
1170
+ gap: 6px;
1171
+
1172
+ &:hover {
1173
+ border-color: #cbd5e1;
1174
+ }
1175
+
1176
+ &:focus-within {
1177
+ border-color: #2563eb;
1178
+ background: #ffffff;
1179
+ }
1180
+ }
1181
+
1182
+ .full-width-pill {
1183
+ width: 100%;
1184
+ }
1185
+
1186
+ .input-icon {
1187
+ width: 14px;
1188
+ height: 14px;
1189
+ color: #64748b;
1190
+ flex-shrink: 0;
1191
+ }
1192
+
1193
+ .custom-num-field {
1194
+ width: 100%;
1195
+ border: none;
1196
+ background: transparent;
1197
+ outline: none;
1198
+ font-size: 13px;
1199
+ font-weight: 500;
1200
+ color: #1e293b;
1201
+ padding: 0;
1202
+
1203
+ &::-webkit-outer-spin-button,
1204
+ &::-webkit-inner-spin-button {
1205
+ -webkit-appearance: none;
1206
+ margin: 0;
1207
+ }
1208
+ }
1209
+
1210
+ /* List Dropdown Split Button */
1211
+ .list-dropdown-btn {
1212
+ display: flex;
1213
+ align-items: center;
1214
+ height: 32px;
1215
+ background: #FAFAFA;
1216
+ border: 1px solid #edf0f3;
1217
+ border-radius: 8px;
1218
+ padding: 0 6px 0 8px;
1219
+ box-sizing: border-box;
1220
+ color: #64748b;
1221
+ cursor: pointer;
1222
+ transition: all 0.15s ease;
1223
+ user-select: none;
1224
+ flex-shrink: 0;
1225
+ gap: 4px;
1226
+
1227
+ &:hover {
1228
+ background: #f8fafc;
1229
+ border-color: #cbd5e1;
1230
+ color: #1e293b;
1231
+ }
1232
+
1233
+ &.is-active {
1234
+ background: #e0ebff;
1235
+ color: #2563eb;
1236
+ border-color: #bfdbfe;
1237
+ }
1238
+ }
1239
+
1240
+ .list-btn-main {
1241
+ display: flex;
1242
+ align-items: center;
1243
+ justify-content: center;
1244
+ height: 100%;
1245
+ }
1246
+
1247
+ .list-btn-arrow {
1248
+ display: flex;
1249
+ align-items: center;
1250
+ justify-content: center;
1251
+ width: 12px;
1252
+ height: 100%;
1253
+ color: #94a3b8;
1254
+ transition: transform 0.2s ease;
1255
+
1256
+ &.is-open {
1257
+ transform: rotate(180deg);
1258
+ }
1259
+ }
1260
+
1261
+ .list-presets-grid {
1262
+ display: grid;
1263
+ grid-template-columns: repeat(3, 1fr);
1264
+ gap: 8px;
1265
+ padding: 4px;
1266
+ }
1267
+
1268
+ .list-style-card {
1269
+ display: flex;
1270
+ align-items: center;
1271
+ justify-content: center;
1272
+ height: 56px;
1273
+ padding: 4px;
1274
+ cursor: pointer;
1275
+ border-radius: 4px;
1276
+ border: 1px solid #e2e8f0;
1277
+ transition: all 0.15s;
1278
+
1279
+ &:hover {
1280
+ border-color: #2563eb;
1281
+ }
1282
+
1283
+ &.active {
1284
+ border-color: #2563eb;
1285
+ background: #eff6ff;
1286
+ }
1287
+
1288
+ img {
1289
+ max-width: 100%;
1290
+ max-height: 100%;
1291
+ object-fit: contain;
1292
+ pointer-events: none;
1293
+ }
1294
+ }
1295
+
1296
+ /* Button Group Pills */
1297
+ .button-group-pill {
1298
+ display: flex;
1299
+ align-items: center;
1300
+ background: #FAFAFA;
1301
+ border: 1px solid #edf0f3;
1302
+ border-radius: 8px;
1303
+ height: 36px;
1304
+ padding: 2px;
1305
+ gap: 2px;
1306
+ box-sizing: border-box;
1307
+ }
1308
+
1309
+ .vertical-align-group {
1310
+ flex: 1;
1311
+ }
1312
+
1313
+ .pill-action-btn {
1314
+ flex: 1;
1315
+ height: 100%;
1316
+ border: none;
1317
+ background: transparent;
1318
+ border-radius: 6px;
1319
+ cursor: pointer;
1320
+ display: flex;
1321
+ align-items: center;
1322
+ justify-content: center;
1323
+ font-size: 13px;
1324
+ font-weight: 500;
1325
+ color: #64748b;
1326
+ transition: all 0.15s ease;
1327
+ user-select: none;
1328
+
1329
+ &:hover {
1330
+ background: rgba(0, 0, 0, 0.04);
1331
+ color: #1e293b;
1332
+ }
1333
+
1334
+ &.is-active {
1335
+ background: #ffffff;
1336
+ color: #2563eb;
1337
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
1338
+ }
1339
+ }
1340
+
1341
+ .font-style-letter {
1342
+ span {
1343
+ transition: color 0.15s ease;
1344
+ }
1345
+ }
1346
+
1347
+ .section-divider {
1348
+ height: 1px;
1349
+ background: #edf0f3;
1350
+ margin: 2px 0;
1351
+ }
1352
+
1353
+ /* Full Width Color Card */
1354
+ .full-color-card {
1355
+ display: flex;
1356
+ align-items: center;
1357
+ justify-content: space-between;
1358
+ width: 100%;
1359
+ height: 32px;
1360
+ background: #FAFAFA;
1361
+ border: 1px solid #edf0f3;
1362
+ border-radius: 8px;
1363
+ padding: 0 12px;
1364
+ box-sizing: border-box;
1365
+ cursor: pointer;
1366
+ transition: all 0.15s ease;
1367
+
1368
+ &:hover {
1369
+ background: #f8fafc;
1370
+ border-color: #cbd5e1;
1371
+ }
1372
+ }
1373
+
1374
+ .color-swatch-hex {
1375
+ display: flex;
1376
+ align-items: center;
1377
+ gap: 10px;
1378
+ }
1379
+
1380
+ .color-swatch-box {
1381
+ width: 16px;
1382
+ height: 16px;
1383
+ border-radius: 4px;
1384
+ border: 1px solid rgba(0, 0, 0, 0.08);
1385
+ flex-shrink: 0;
1386
+ }
1387
+
1388
+ .color-hex-text {
1389
+ font-size: 13px;
1390
+ font-weight: 500;
1391
+ font-family: monospace;
1392
+ color: #1e293b;
1393
+ }
1394
+
1395
+ .color-opacity-text {
1396
+ font-size: 13px;
1397
+ font-weight: 500;
1398
+ color: #1e293b;
1399
+ }
1400
+
1401
+ /* Accordion Block */
1402
+ .accordion-item-block {
1403
+ display: flex;
1404
+ flex-direction: column;
1405
+ }
1406
+
1407
+ .accordion-header-row {
1408
+ display: flex;
1409
+ align-items: center;
1410
+ justify-content: space-between;
1411
+ height: 32px;
1412
+ cursor: pointer;
1413
+ user-select: none;
1414
+ }
1415
+
1416
+ .accordion-title-wrap {
1417
+ display: flex;
1418
+ align-items: center;
1419
+ gap: 6px;
1420
+ }
1421
+
1422
+ .accordion-title {
1423
+ font-size: 14px;
1424
+ font-weight: 400;
1425
+ color: #1e293b;
1426
+ }
1427
+
1428
+ .accordion-arrow {
1429
+ font-size: 8px;
1430
+ color: #1e293b;
1431
+ display: inline-block;
1432
+ transition: transform 0.2s ease;
1433
+ transform-origin: center;
1434
+ }
1435
+
1436
+ .accordion-inner-content {
1437
+ display: flex;
1438
+ flex-direction: column;
1439
+ gap: 8px;
1440
+ padding-top: 6px;
1441
+ }
1442
+
1443
+ /* Padding Grid & Lock */
1444
+ .padding-control-container {
1445
+ display: flex;
1446
+ gap: 8px;
1447
+ align-items: flex-end;
1448
+ }
1449
+
1450
+ .padding-inputs-grid {
1451
+ display: grid;
1452
+ grid-template-columns: 1fr 1fr;
1453
+ gap: 8px;
1454
+ flex: 1;
1455
+ }
1456
+
1457
+ .padding-lock-btn {
1458
+ width: 32px;
1459
+ height: 32px;
1460
+ background: #FAFAFA;
1461
+ border: 1px solid #edf0f3;
1462
+ border-radius: 8px;
1463
+ display: flex;
1464
+ align-items: center;
1465
+ justify-content: center;
1466
+ cursor: pointer;
1467
+ flex-shrink: 0;
1468
+ transition: all 0.15s ease;
1469
+ color: #64748b;
1470
+
1471
+ &:not(.is-locked):hover {
1472
+ background: rgba(30, 111, 255, 0.12);
1473
+ border-color: rgba(30, 111, 255, 0.4);
1474
+ color: #1E6FFF;
1475
+ }
1476
+
1477
+ &.is-locked,
1478
+ &.is-locked:hover {
1479
+ background: #1E6FFF !important;
1480
+ border-color: #1E6FFF !important;
1481
+ color: #ffffff !important;
1482
+ }
1483
+ }
1484
+
1485
+ /* Slider rows for Shadow & Reflection */
1486
+ .shape-property-row {
1487
+ display: flex;
1488
+ align-items: center;
1489
+ justify-content: space-between;
1490
+ height: 32px;
1491
+ gap: 8px;
1492
+ }
1493
+
1494
+ .slider-prop-label {
1495
+ font-size: 12px;
1496
+ color: #64748b;
1497
+ width: 60px;
1498
+ flex-shrink: 0;
1499
+ text-align: left;
1500
+ }
1501
+
1502
+ .shape-slider-wrapper {
1503
+ display: flex;
1504
+ align-items: center;
1505
+ gap: 10px;
1506
+ flex: 1;
1507
+ }
1508
+
1509
+ :deep(.custom-slider) {
1510
+ --el-slider-main-bg-color: #334155 !important;
1511
+ --el-slider-runway-bg-color: #e2e8f0 !important;
1512
+ --el-slider-height: 4px !important;
1513
+ --el-slider-button-size: 14px !important;
1514
+
1515
+ .el-slider__button {
1516
+ border: 1px solid #d1d5db !important;
1517
+ background-color: #ffffff !important;
1518
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12) !important;
1519
+ }
1520
+
1521
+ .el-input-number {
1522
+ width: 48px;
1523
+ height: 30px;
1524
+
1525
+ .el-input__wrapper {
1526
+ padding: 0 4px;
1527
+ background: #FAFAFA !important;
1528
+ border: 1px solid #edf0f3 !important;
1529
+ border-radius: 8px !important;
1530
+ box-shadow: none !important;
1531
+ }
1532
+
1533
+ .el-input__inner {
1534
+ text-align: center;
1535
+ font-size: 12px;
1536
+ color: #1e293b;
1537
+ }
1538
+ }
1539
+ }
1540
+
1541
+ .angle-icon {
1542
+ position: absolute;
1543
+ top: 50%;
1544
+ transform: translateY(calc(-50% - 2px));
1545
+ pointer-events: none;
1546
+ font-size: 12px;
1547
+ color: #64748b;
1548
+ margin-left: 2px;
1549
+ }
1550
+
1551
+ .angle-dial {
1552
+ width: 32px;
1553
+ height: 32px;
1554
+ border: 1px solid #edf0f3;
1555
+ border-radius: 50%;
1556
+ display: flex;
1557
+ align-items: center;
1558
+ justify-content: center;
1559
+ cursor: pointer;
1560
+ position: relative;
1561
+ background: #ffffff;
1562
+ flex-shrink: 0;
1563
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
1564
+ user-select: none;
1565
+ }
1566
+
1567
+ .dial-needle {
1568
+ width: 100%;
1569
+ height: 2px;
1570
+ position: absolute;
1571
+ transform-origin: center;
1572
+ display: flex;
1573
+ justify-content: flex-end;
1574
+ align-items: center;
1575
+ }
1576
+
1577
+ .dial-needle-tip {
1578
+ width: 16px;
1579
+ height: 2px;
1580
+ background: #6F7588;
1581
+ }
1582
+
1583
+ .dial-center-dot {
1584
+ position: absolute;
1585
+ width: 4px;
1586
+ height: 4px;
1587
+ background-color: #6F7588;
1588
+ border-radius: 50%;
1589
+ top: 50%;
1590
+ left: 50%;
1591
+ transform: translate(-50%, -50%);
1592
+ pointer-events: none;
1593
+ }
1594
+
1595
+ :deep(.custom-select) {
1596
+ .el-select__wrapper {
1597
+ background-color: #FAFAFA !important;
1598
+ border: 1px solid #edf0f3 !important;
1599
+ box-shadow: none !important;
1600
+ height: 32px !important;
1601
+ min-height: 32px !important;
1602
+ max-height: 32px !important;
1603
+ line-height: 30px !important;
1604
+ padding: 0 10px !important;
1605
+ border-radius: 8px !important;
1606
+ transition: all 0.15s ease;
1607
+
1608
+ &:hover {
1609
+ border-color: #cbd5e1 !important;
1610
+ }
1611
+
1612
+ &.is-focused {
1613
+ border-color: #2563eb !important;
1614
+ background-color: #ffffff !important;
1615
+ }
1616
+ }
1617
+
1618
+ /* Selected item text color: crisp dark #1e293b */
1619
+ .el-select__selected-item,
1620
+ .el-select__placeholder,
1621
+ .el-select__selected-item span,
1622
+ .el-select__placeholder span,
1623
+ .el-select__selection span,
1624
+ .el-input__inner,
1625
+ .el-select__input {
1626
+ font-size: 13px !important;
1627
+ font-weight: 500 !important;
1628
+ color: #1e293b !important;
1629
+ -webkit-text-fill-color: #1e293b !important;
1630
+ }
1631
+
1632
+ /* Only when truly empty placeholder */
1633
+ .el-select__placeholder.is-placeholder,
1634
+ .el-select__wrapper.is-empty .el-select__placeholder,
1635
+ .el-select__wrapper.is-empty .el-select__placeholder span {
1636
+ font-size: 13px !important;
1637
+ font-weight: 400 !important;
1638
+ color: #94a3b8 !important;
1639
+ -webkit-text-fill-color: #94a3b8 !important;
1640
+ }
1641
+
1642
+ /* Suffix icon */
1643
+ .el-select__caret,
1644
+ .el-select__suffix .el-icon {
1645
+ color: #94a3b8 !important;
1646
+ font-size: 10px !important;
1647
+ transition: transform 0.2s ease !important;
1648
+ }
1649
+
1650
+ .el-select__wrapper.is-focused .el-select__caret,
1651
+ .el-select__wrapper.is-focused .el-select__suffix .el-icon {
1652
+ transform: rotate(180deg) !important;
1653
+ }
1654
+ }
1655
+
1656
+ :deep(.el-switch__core) {
1657
+ border-radius: 12px !important;
1658
+ }
1659
+
1660
+ :deep(.color-span-box) {
1661
+ width: 100%;
1662
+ }
1663
+
1664
+ /* Custom Select Trigger for AutoFit */
1665
+ .custom-select-trigger {
1666
+ display: flex;
1667
+ align-items: center;
1668
+ justify-content: space-between;
1669
+ width: 100%;
1670
+ height: 36px;
1671
+ background-color: #FAFAFA;
1672
+ border: 1px solid #edf0f3;
1673
+ border-radius: 8px;
1674
+ padding: 0 10px;
1675
+ box-sizing: border-box;
1676
+ cursor: pointer;
1677
+ transition: all 0.15s ease;
1678
+ user-select: none;
1679
+
1680
+ &:hover {
1681
+ border-color: #cbd5e1;
1682
+ }
1683
+
1684
+ &.is-active {
1685
+ border-color: #2563eb;
1686
+ background-color: #ffffff;
1687
+ }
1688
+ }
1689
+
1690
+ .select-trigger-text {
1691
+ font-size: 13px;
1692
+ font-weight: 500;
1693
+ color: #1e293b;
1694
+ }
1695
+
1696
+ .select-trigger-arrow {
1697
+ color: #94a3b8;
1698
+ transition: transform 0.2s ease;
1699
+
1700
+ &.is-open {
1701
+ transform: rotate(180deg);
1702
+ }
1703
+ }
1704
+
1705
+ .font-size-trigger {
1706
+ cursor: text;
1707
+ }
1708
+
1709
+ .custom-select-input {
1710
+ width: 100%;
1711
+ border: none;
1712
+ background: transparent;
1713
+ outline: none;
1714
+ font-size: 13px;
1715
+ font-weight: 500;
1716
+ color: #1e293b;
1717
+ padding: 0;
1718
+ }
1719
+
1720
+ .font-size-menu-container {
1721
+ display: flex;
1722
+ flex-direction: column;
1723
+ gap: 2px;
1724
+ padding-right: 2px;
1725
+ }
1726
+
1727
+ .font-size-item {
1728
+ height: 32px !important;
1729
+ padding: 0 10px !important;
1730
+ }
1731
+
1732
+ .autofit-menu-container {
1733
+ display: flex;
1734
+ flex-direction: column;
1735
+ gap: 2px;
1736
+ }
1737
+
1738
+ .autofit-menu-item {
1739
+ display: flex;
1740
+ align-items: center;
1741
+ justify-content: space-between;
1742
+ height: 34px;
1743
+ min-height: 34px;
1744
+ line-height: 34px;
1745
+ flex-shrink: 0;
1746
+ box-sizing: border-box;
1747
+ padding: 0 10px;
1748
+ border-radius: 6px;
1749
+ cursor: pointer;
1750
+ transition: all 0.15s ease;
1751
+ user-select: none;
1752
+
1753
+ &:hover {
1754
+ background-color: #f8fafc;
1755
+ }
1756
+
1757
+ &.is-selected {
1758
+ background-color: #eef2ff;
1759
+
1760
+ .autofit-item-label {
1761
+ color: #2563eb;
1762
+ font-weight: 500;
1763
+ }
1764
+ }
1765
+ }
1766
+
1767
+ .autofit-item-label {
1768
+ font-size: 13px;
1769
+ color: #1e293b;
1770
+ transition: color 0.15s ease;
1771
+ }
1772
+
1773
+ .autofit-check-icon {
1774
+ color: #2563eb;
1775
+ flex-shrink: 0;
1776
+ }
1777
+
1778
+ .autofit-divider {
1779
+ height: 1px;
1780
+ background-color: #edf0f3;
1781
+ margin: 4px 0;
1782
+ }
1783
+
1784
+ .autofit-wrap-row {
1785
+ display: flex;
1786
+ align-items: center;
1787
+ justify-content: space-between;
1788
+ height: 36px;
1789
+ padding: 0 10px;
1790
+ user-select: none;
1791
+ }
1792
+
1793
+ .autofit-wrap-label {
1794
+ font-size: 13px;
1795
+ color: #1e293b;
1796
+ }
1797
+ </style>
1798
+
1799
+ <style lang="scss">
1800
+ .autofit-popover-panel,
1801
+ .font-size-popover {
1802
+ min-width: 0 !important;
1803
+ padding: 6px !important;
1804
+ border-radius: 10px !important;
1805
+ border: 1px solid #edf0f3 !important;
1806
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
1807
+ background: #ffffff !important;
1808
+ z-index: 9999 !important;
1809
+
1810
+ .el-popper__arrow {
1811
+ display: none !important;
1812
+ }
1813
+ }
1814
+
1815
+ .font-size-popover {
1816
+ min-width: 0 !important;
1817
+ box-sizing: border-box !important;
1818
+ padding: 6px 4px 6px 6px !important;
1819
+ background: #ffffff !important;
1820
+
1821
+ .el-scrollbar {
1822
+ padding-right: 0 !important;
1823
+ }
1824
+
1825
+ .el-scrollbar__bar.is-vertical {
1826
+ width: 4px !important;
1827
+ right: 1px !important;
1828
+ }
1829
+
1830
+ .el-scrollbar__thumb {
1831
+ background-color: #cbd5e1 !important;
1832
+ border-radius: 4px !important;
1833
+
1834
+ &:hover {
1835
+ background-color: #94a3b8 !important;
1836
+ }
1837
+ }
1838
+ }
1839
+
1840
+ .el-popper.sidebar-select-popper {
1841
+ min-width: 0 !important;
1842
+ padding: 0 !important;
1843
+ border-radius: 10px !important;
1844
+ border: 1px solid #edf0f3 !important;
1845
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
1846
+ background: #ffffff !important;
1847
+ overflow: hidden !important;
1848
+ z-index: 9999 !important;
1849
+
1850
+ .el-popper__arrow {
1851
+ display: none !important;
1852
+ }
1853
+
1854
+ .el-select-dropdown {
1855
+ padding: 6px !important;
1856
+ margin: 0 !important;
1857
+ border: none !important;
1858
+ box-shadow: none !important;
1859
+ border-radius: 0 !important;
1860
+ background: transparent !important;
1861
+ box-sizing: border-box !important;
1862
+ }
1863
+
1864
+ .el-select-dropdown__wrap {
1865
+ max-height: 274px !important;
1866
+ }
1867
+
1868
+ .el-scrollbar__bar.is-vertical {
1869
+ width: 4px !important;
1870
+ right: 2px !important;
1871
+
1872
+ .el-scrollbar__thumb {
1873
+ background-color: #cbd5e1 !important;
1874
+ border-radius: 2px !important;
1875
+ }
1876
+ }
1877
+
1878
+ .el-select-dropdown__list {
1879
+ padding: 0 !important;
1880
+ margin: 0 !important;
1881
+ display: flex !important;
1882
+ flex-direction: column !important;
1883
+ gap: 2px !important;
1884
+ }
1885
+
1886
+ .el-select-dropdown__item {
1887
+ height: 34px !important;
1888
+ min-height: 34px !important;
1889
+ line-height: 34px !important;
1890
+ flex-shrink: 0 !important;
1891
+ box-sizing: border-box !important;
1892
+ padding: 0 10px !important;
1893
+ border-radius: 6px !important;
1894
+ font-size: 13px !important;
1895
+ color: #1e293b !important;
1896
+ display: flex !important;
1897
+ align-items: center !important;
1898
+ justify-content: space-between !important;
1899
+ transition: all 0.15s ease !important;
1900
+ user-select: none !important;
1901
+
1902
+ &:hover,
1903
+ &.hover,
1904
+ &.is-hovering {
1905
+ background-color: #f8fafc !important;
1906
+ color: #1e293b !important;
1907
+ }
1908
+
1909
+ &.is-selected {
1910
+ background-color: #eef2ff !important;
1911
+ color: #2563eb !important;
1912
+ font-weight: 500 !important;
1913
+
1914
+ &::after {
1915
+ content: '';
1916
+ display: inline-block;
1917
+ width: 15px;
1918
+ height: 15px;
1919
+ flex-shrink: 0;
1920
+ margin-left: 8px;
1921
+ background-color: #2563eb;
1922
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E") no-repeat center / contain;
1923
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E") no-repeat center / contain;
1924
+ }
1925
+ }
1926
+ }
1927
+ }
1928
+
1929
+ .el-select-dropdown.sidebar-select-popper {
1930
+ border: none !important;
1931
+ box-shadow: none !important;
1932
+ border-radius: 0 !important;
1933
+ background: transparent !important;
1934
+ padding: 6px !important;
1935
+ margin: 0 !important;
1936
+ box-sizing: border-box !important;
1937
+ }
1938
+ </style>