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.
- package/dist/lib/check_imgs.html +18 -0
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.css +187 -27
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.js +1 -1
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.json +345 -65
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.ttf +0 -0
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff +0 -0
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff2 +0 -0
- package/dist/lib/jvs-ppt-ui/img/al.png +0 -0
- package/dist/lib/jvs-ppt-ui/img/d.png +0 -0
- package/dist/lib/jvs-ppt-ui/img/dl.png +0 -0
- package/dist/lib/jvs-ppt-ui/img/l.png +0 -0
- package/dist/lib/jvs-ppt-ui.es.js +44973 -43349
- package/dist/lib/jvs-ppt-ui.umd.js +102 -102
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/scripts/check-icons.mjs +22 -0
- package/scripts/patch-anim-icons.mjs +45 -0
- package/src/assets/iconfont/iconfont.css +187 -27
- package/src/assets/iconfont/iconfont.js +1 -1
- package/src/assets/iconfont/iconfont.json +345 -65
- package/src/assets/iconfont/iconfont.ttf +0 -0
- package/src/assets/iconfont/iconfont.woff +0 -0
- package/src/assets/iconfont/iconfont.woff2 +0 -0
- package/src/components/CanvasArea.vue +218 -193
- package/src/components/ChartTypeSelector.vue +509 -148
- package/src/components/Header.vue +327 -44
- package/src/components/PresentationGroupChild.vue +44 -13
- package/src/components/PresentationMode.vue +84 -33
- package/src/components/QuickBorderPopover.vue +51 -11
- package/src/components/QuickColorPopover.vue +65 -21
- package/src/components/SidebarLeft.vue +286 -107
- package/src/components/SidebarRight.vue +45 -38
- package/src/components/TemplateGallery.vue +1110 -1237
- package/src/components/ToastNotification.vue +2 -2
- package/src/components/Toolbar.vue +825 -733
- package/src/components/canvas/CanvasQuickToolbar.vue +17 -30
- package/src/components/canvas/elements/CanvasGroupElement.vue +6 -0
- package/src/components/canvas/elements/CanvasShapeElement.vue +198 -38
- package/src/components/canvas/elements/CanvasTextElement.vue +4 -3
- package/src/components/sidebar/common/SidebarAlignGroup.vue +104 -50
- package/src/components/sidebar/common/SidebarBorderAccordion.vue +375 -148
- package/src/components/sidebar/common/SidebarPositionGroup.vue +358 -149
- package/src/components/sidebar/common/SidebarReflectionAccordion.vue +429 -203
- package/src/components/sidebar/common/SidebarShadowAccordion.vue +844 -416
- package/src/components/sidebar/panels/GradientEditor.vue +671 -316
- package/src/components/sidebar/panels/LayoutThumbSvg.vue +33 -16
- package/src/components/sidebar/panels/SidebarAnimationPanel.vue +701 -138
- package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
- package/src/components/sidebar/panels/SidebarDesignPanel.vue +1754 -416
- package/src/components/sidebar/panels/SidebarMediaFormatPanel.vue +12 -0
- package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +813 -174
- package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
- package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1938 -1427
- package/src/components/sidebar/panels/SidebarTransitionPanel.vue +791 -210
- package/src/composables/canvas/useCanvasShortcuts.ts +20 -6
- package/src/composables/canvas/useCanvasText.ts +1 -1
- package/src/composables/canvas/useCanvasTransform.ts +44 -0
- package/src/composables/presetThemes.ts +43 -44
- package/src/composables/sidebar/useSidebarAnimation.ts +101 -25
- package/src/composables/sidebar/useSidebarDesign.ts +30 -11
- package/src/composables/sidebar/useSidebarElementTransform.ts +10 -1
- package/src/composables/sidebar/useSidebarImage.ts +10 -2
- package/src/composables/sidebar/useSidebarShapeFormat.ts +562 -107
- package/src/composables/useSlides.ts +331 -153
- package/src/main.ts +2 -0
- package/src/style.css +926 -359
- package/src/utils/canvas/elementStyles.ts +42 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, watch, defineAsyncComponent } from 'vue'
|
|
2
|
+
import { ref, computed, watch, nextTick, defineAsyncComponent } from 'vue'
|
|
3
3
|
// 样式构建器大弹层(~100KB 源码)异步化:首次打开才加载
|
|
4
4
|
const StyleBuilderDialog = defineAsyncComponent(() => import('../../StyleBuilderDialog.vue'))
|
|
5
5
|
import LayoutThumbSvg from './LayoutThumbSvg.vue'
|
|
@@ -7,6 +7,7 @@ import QuickColorPopover from '../../QuickColorPopover.vue'
|
|
|
7
7
|
import GradientEditor from './GradientEditor.vue'
|
|
8
8
|
import { useSlides, gradientPresets, normalizeSlideBackground } from '../../../composables/useSlides'
|
|
9
9
|
import { SYS_FONTS } from '../../../configs/fonts'
|
|
10
|
+
import { PATTERN_PRESETS, parseColor, hexToRgba, generateGradientEndColor, buildPatternSvgDataUrl, parsePatternSvgMeta } from '../../../composables/sidebar/useSidebarShapeFormat'
|
|
10
11
|
|
|
11
12
|
const props = defineProps<{
|
|
12
13
|
designActiveTab: string
|
|
@@ -56,16 +57,23 @@ const {
|
|
|
56
57
|
activeMasterId,
|
|
57
58
|
slideMasters,
|
|
58
59
|
getThemeUsageLabel,
|
|
60
|
+
getThemeUsageSlides,
|
|
59
61
|
addHistorySnapshot,
|
|
60
|
-
uploadResource
|
|
62
|
+
uploadResource,
|
|
63
|
+
logicalWidth,
|
|
64
|
+
logicalHeight
|
|
61
65
|
} = useSlides()
|
|
62
66
|
|
|
63
67
|
const currentActiveThemeId = computed(() => {
|
|
68
|
+
let themeId = ''
|
|
64
69
|
if (isMasterEditingView.value) {
|
|
65
70
|
const master = slideMasters.value.find(m => m.id === activeMasterId.value) || slideMasters.value[0]
|
|
66
|
-
|
|
71
|
+
themeId = (master as any)?.themeId || 'tech-blue'
|
|
72
|
+
} else {
|
|
73
|
+
themeId = props.slides[props.activeIndex]?.themeId || 'tech-blue'
|
|
67
74
|
}
|
|
68
|
-
|
|
75
|
+
if (themeId === 'default-recipe') return 'tech-blue'
|
|
76
|
+
return themeId
|
|
69
77
|
})
|
|
70
78
|
|
|
71
79
|
// Quick Color Popover recent colors
|
|
@@ -73,20 +81,287 @@ const quickRecentColors = ref<string[]>([])
|
|
|
73
81
|
const globalFontColorPopoverVisible = ref(false)
|
|
74
82
|
const globalBorderColorPopoverVisible = ref(false)
|
|
75
83
|
const bgColorPopoverVisible = ref(false)
|
|
84
|
+
const bgPatternFgPopoverVisible = ref(false)
|
|
85
|
+
const bgPatternBgPopoverVisible = ref(false)
|
|
76
86
|
const isCustomColorPickerOpen = ref(false)
|
|
77
87
|
|
|
88
|
+
// 画布尺寸下拉 Popover 状态
|
|
89
|
+
const isRatioPopoverVisible = ref(false)
|
|
90
|
+
const ratioTriggerRef = ref<HTMLElement | null>(null)
|
|
91
|
+
const ratioTriggerWidth = ref(258)
|
|
92
|
+
|
|
93
|
+
const ratioOptions = [
|
|
94
|
+
{ label: '宽屏 16:9', value: '16:9' },
|
|
95
|
+
{ label: '正宽 4:3', value: '4:3' },
|
|
96
|
+
{ label: '宽屏 16:10', value: '16:10' },
|
|
97
|
+
{ label: '手机竖屏 9:16 (移动端)', value: '9:16' }
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const currentRatioLabel = computed(() => {
|
|
101
|
+
const match = ratioOptions.find(opt => opt.value === props.slideRatio)
|
|
102
|
+
if (match) return match.label
|
|
103
|
+
if (props.slideRatio === 'custom') return '自定义尺寸'
|
|
104
|
+
return props.slideRatio || '宽屏 16:9'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function onRatioPopoverShow() {
|
|
108
|
+
if (ratioTriggerRef.value) {
|
|
109
|
+
ratioTriggerWidth.value = ratioTriggerRef.value.offsetWidth
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function selectRatio(val: string) {
|
|
114
|
+
emit('update:slideRatio', val)
|
|
115
|
+
isRatioPopoverVisible.value = false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 背景图片适配模式下拉 Popover 状态
|
|
119
|
+
const isImageModePopoverVisible = ref(false)
|
|
120
|
+
const imageModeTriggerRef = ref<HTMLElement | null>(null)
|
|
121
|
+
const imageModeTriggerWidth = ref(180)
|
|
122
|
+
|
|
123
|
+
const imageModeOptions = [
|
|
124
|
+
{ label: '拉伸铺满', value: 'stretch' },
|
|
125
|
+
{ label: '等比缩放', value: 'contain' },
|
|
126
|
+
{ label: '居中', value: 'cover' },
|
|
127
|
+
{ label: '平铺', value: 'tile' }
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
const currentImageModeLabel = computed(() => {
|
|
131
|
+
const match = imageModeOptions.find(opt => opt.value === bgImageMode.value)
|
|
132
|
+
return match ? match.label : '拉伸铺满'
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
function onImageModePopoverShow() {
|
|
136
|
+
if (imageModeTriggerRef.value) {
|
|
137
|
+
imageModeTriggerWidth.value = imageModeTriggerRef.value.offsetWidth
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function selectImageMode(val: string) {
|
|
142
|
+
bgImageMode.value = val as any
|
|
143
|
+
isImageModePopoverVisible.value = false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 字体下拉 Popover 状态
|
|
147
|
+
const isFontPopoverVisible = ref(false)
|
|
148
|
+
const fontTriggerRef = ref<HTMLElement | null>(null)
|
|
149
|
+
const fontTriggerWidth = ref(258)
|
|
150
|
+
|
|
151
|
+
const currentFontFamilyLabel = computed(() => {
|
|
152
|
+
if (!globalFontFamily.value || globalFontFamily.value === '默认字体') return '默认字体'
|
|
153
|
+
return fontLabel(globalFontFamily.value)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
function onFontPopoverShow() {
|
|
157
|
+
if (fontTriggerRef.value) {
|
|
158
|
+
fontTriggerWidth.value = fontTriggerRef.value.offsetWidth
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function selectFont(fontVal: string) {
|
|
163
|
+
globalFontFamily.value = fontVal
|
|
164
|
+
isFontPopoverVisible.value = false
|
|
165
|
+
}
|
|
166
|
+
|
|
78
167
|
// 主题色色盘 Popover
|
|
79
168
|
const themePalettePopoverVisible = ref(false)
|
|
80
169
|
const globalThemeColorPopoverVisible = ref<boolean[]>(Array(5).fill(false))
|
|
81
170
|
|
|
82
|
-
// 格式化 hex 显示(去掉 #
|
|
83
|
-
const formatHexDisplay = (color:
|
|
84
|
-
if (!color
|
|
85
|
-
|
|
171
|
+
// 格式化 hex 显示(去掉 # 并大写,保留 6 位纯色)
|
|
172
|
+
const formatHexDisplay = (color: any) => {
|
|
173
|
+
if (!color) return 'FFFFFF'
|
|
174
|
+
let str = ''
|
|
175
|
+
if (typeof color === 'string') {
|
|
176
|
+
str = color
|
|
177
|
+
} else if (typeof color === 'object' && color !== null) {
|
|
178
|
+
if (color.type === 'color' && typeof color.value === 'string') {
|
|
179
|
+
str = color.value
|
|
180
|
+
} else if (typeof color.value === 'string') {
|
|
181
|
+
str = color.value
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!str || str === 'transparent' || str === 'none') return 'FFFFFF'
|
|
185
|
+
const parsed = parseColor(str)
|
|
186
|
+
let clean = (parsed.hex || str).replace('#', '').trim().toUpperCase()
|
|
187
|
+
if (clean.length === 8) clean = clean.slice(0, 6)
|
|
188
|
+
return clean
|
|
86
189
|
}
|
|
87
190
|
|
|
88
191
|
// 字体显示名称
|
|
89
|
-
const fontLabel = (font
|
|
192
|
+
const fontLabel = (font?: string) => {
|
|
193
|
+
if (!font) return '思源黑体 CN'
|
|
194
|
+
if (font === 'SourceHanSansCN' || font === '思源黑体') return '思源黑体 CN'
|
|
195
|
+
return SYS_FONTS.find(f => f.value === font || f.label === font)?.label || font
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const getThemeSlideCount = (recipeId: string) => {
|
|
199
|
+
const pages = getThemeUsageSlides(recipeId)
|
|
200
|
+
return pages ? pages.length : 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const getThemeFontColors = (recipe: any) => {
|
|
204
|
+
if (recipe.id === 'tech-blue') return { aa: '#2563eb', font: '#64748b' }
|
|
205
|
+
if (recipe.id === 'fresh-green') return { aa: '#047857', font: '#059669' }
|
|
206
|
+
if (recipe.id === 'fashion-purple') return { aa: '#7c3aed', font: '#8b5cf6' }
|
|
207
|
+
if (recipe.id === 'dark-gold') return { aa: '#fbbf24', font: '#d97706' }
|
|
208
|
+
if (recipe.id === 'warm-amber') return { aa: '#ea580c', font: '#c2410c' }
|
|
209
|
+
|
|
210
|
+
const isDark = recipe.backgroundColor && (recipe.backgroundColor === '#121826' || recipe.backgroundColor === '#0f172a' || recipe.backgroundColor === '#18181b')
|
|
211
|
+
return {
|
|
212
|
+
aa: recipe.styleColors?.[0] || '#2563eb',
|
|
213
|
+
font: isDark ? '#e2e8f0' : (recipe.textStyles?.subheadline?.color || '#64748b')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function handleSelectTheme(recipe: any) {
|
|
218
|
+
emit('applyStyleRecipeToSlides', recipe, false)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleEditTheme(recipe: any) {
|
|
222
|
+
emit('openStyleBuilder', isMasterEditingView.value ? 'all' : 'single', recipe.id)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function handleDuplicateTheme(recipe: any) {
|
|
226
|
+
const newRecipe = JSON.parse(JSON.stringify(recipe))
|
|
227
|
+
newRecipe.id = 'theme-' + Date.now()
|
|
228
|
+
newRecipe.name = `${recipe.name} 副本`
|
|
229
|
+
const list = props.builtinStyleRecipes
|
|
230
|
+
const idx = list.findIndex((r: any) => r.id === recipe.id)
|
|
231
|
+
if (idx >= 0) {
|
|
232
|
+
list.splice(idx + 1, 0, newRecipe)
|
|
233
|
+
} else {
|
|
234
|
+
list.push(newRecipe)
|
|
235
|
+
}
|
|
236
|
+
notify?.(`已复制主题「${recipe.name}」`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handleApplyToAllSlides(recipe: any) {
|
|
240
|
+
emit('applyStyleRecipeToSlides', recipe, true)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function handleDeleteTheme(recipe: any) {
|
|
244
|
+
const list = props.builtinStyleRecipes
|
|
245
|
+
const idx = list.findIndex((r: any) => r.id === recipe.id)
|
|
246
|
+
if (idx >= 0) {
|
|
247
|
+
list.splice(idx, 1)
|
|
248
|
+
notify?.(`已删除主题「${recipe.name}」`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 缓存背景图片的原始宽高,用于居中/平铺模式的精确比例计算
|
|
253
|
+
const bgImageNaturalSizes = ref<Record<string, { w: number; h: number }>>({})
|
|
254
|
+
const loadingBgImageUrls = new Set<string>()
|
|
255
|
+
|
|
256
|
+
function extractUrlFromBgCss(bg: any): string {
|
|
257
|
+
if (!bg) return ''
|
|
258
|
+
if (typeof bg === 'string') {
|
|
259
|
+
const startIdx = bg.indexOf('url(')
|
|
260
|
+
if (startIdx === -1) return ''
|
|
261
|
+
const closeIdx = bg.lastIndexOf(')')
|
|
262
|
+
if (closeIdx <= startIdx + 4) return ''
|
|
263
|
+
let inner = bg.slice(startIdx + 4, closeIdx).trim()
|
|
264
|
+
if ((inner.startsWith('"') && inner.endsWith('"')) || (inner.startsWith("'") && inner.endsWith("'"))) {
|
|
265
|
+
inner = inner.slice(1, -1).trim()
|
|
266
|
+
}
|
|
267
|
+
return inner
|
|
268
|
+
}
|
|
269
|
+
if (typeof bg === 'object' && bg !== null) {
|
|
270
|
+
const norm = normalizeSlideBackground(bg)
|
|
271
|
+
if (norm.bgType === 'image') {
|
|
272
|
+
return extractUrlFromBgCss(norm.background)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return ''
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function ensureBgImageSizeLoaded(url: string) {
|
|
279
|
+
if (!url || url.startsWith('data:image/svg+xml') || bgImageNaturalSizes.value[url] || loadingBgImageUrls.has(url)) {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
loadingBgImageUrls.add(url)
|
|
283
|
+
const img = new Image()
|
|
284
|
+
img.onload = () => {
|
|
285
|
+
loadingBgImageUrls.delete(url)
|
|
286
|
+
const w = img.naturalWidth || img.width
|
|
287
|
+
const h = img.naturalHeight || img.height
|
|
288
|
+
if (w > 0 && h > 0) {
|
|
289
|
+
bgImageNaturalSizes.value = {
|
|
290
|
+
...bgImageNaturalSizes.value,
|
|
291
|
+
[url]: { w, h }
|
|
292
|
+
}
|
|
293
|
+
// 若当前幻灯片正在使用该图片且处于居中或平铺模式,按真实尺寸比例刷新背景 CSS
|
|
294
|
+
const curUrl = extractUrlFromBgCss(activeSlide.value?.background)
|
|
295
|
+
const curMode = bgImageMode.value
|
|
296
|
+
if (activeSlide.value && curUrl === url && (curMode === 'cover' || curMode === 'tile')) {
|
|
297
|
+
const formatted = formatSlideBgImageCss(url, curMode)
|
|
298
|
+
if (activeSlide.value.background !== formatted) {
|
|
299
|
+
activeSlide.value.background = formatted
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
img.onerror = () => {
|
|
305
|
+
loadingBgImageUrls.delete(url)
|
|
306
|
+
}
|
|
307
|
+
img.src = url
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function formatSlideBgImageCss(url: string, mode: string): string {
|
|
311
|
+
if (!url) return ''
|
|
312
|
+
if (url.startsWith('data:image/svg+xml')) {
|
|
313
|
+
if (mode === 'stretch') return `url("${url}") center center / 100% 100% no-repeat`
|
|
314
|
+
if (mode === 'contain') return `url("${url}") center center / contain no-repeat`
|
|
315
|
+
if (mode === 'cover') return `url("${url}") center center / auto no-repeat`
|
|
316
|
+
return `url("${url}") left top / auto repeat`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (mode === 'stretch') {
|
|
320
|
+
return `url("${url}") center center / 100% 100% no-repeat`
|
|
321
|
+
}
|
|
322
|
+
if (mode === 'contain') {
|
|
323
|
+
return `url("${url}") center center / contain no-repeat`
|
|
324
|
+
}
|
|
325
|
+
if (mode === 'cover') {
|
|
326
|
+
ensureBgImageSizeLoaded(url)
|
|
327
|
+
const dim = bgImageNaturalSizes.value[url]
|
|
328
|
+
const slideW = logicalWidth?.value || 1280
|
|
329
|
+
const slideH = logicalHeight?.value || 720
|
|
330
|
+
if (dim && dim.w > 0 && dim.h > 0) {
|
|
331
|
+
if (dim.w <= slideW * 0.75 && dim.h <= slideH * 0.75) {
|
|
332
|
+
const wPct = ((dim.w / slideW) * 100).toFixed(2)
|
|
333
|
+
const hPct = ((dim.h / slideH) * 100).toFixed(2)
|
|
334
|
+
return `url("${url}") center center / ${wPct}% ${hPct}% no-repeat`
|
|
335
|
+
} else {
|
|
336
|
+
const scale = Math.min((slideW * 0.6) / dim.w, (slideH * 0.6) / dim.h)
|
|
337
|
+
const wPct = ((dim.w * scale / slideW) * 100).toFixed(2)
|
|
338
|
+
const hPct = ((dim.h * scale / slideH) * 100).toFixed(2)
|
|
339
|
+
return `url("${url}") center center / ${wPct}% ${hPct}% no-repeat`
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return `url("${url}") center center / 60% auto no-repeat`
|
|
343
|
+
}
|
|
344
|
+
if (mode === 'tile') {
|
|
345
|
+
ensureBgImageSizeLoaded(url)
|
|
346
|
+
const dim = bgImageNaturalSizes.value[url]
|
|
347
|
+
const slideW = logicalWidth?.value || 1280
|
|
348
|
+
const slideH = logicalHeight?.value || 720
|
|
349
|
+
if (dim && dim.w > 0 && dim.h > 0) {
|
|
350
|
+
if (dim.w <= slideW * 0.35 && dim.h <= slideH * 0.35) {
|
|
351
|
+
const wPct = ((dim.w / slideW) * 100).toFixed(2)
|
|
352
|
+
const hPct = ((dim.h / slideH) * 100).toFixed(2)
|
|
353
|
+
return `url("${url}") left top / ${wPct}% ${hPct}% repeat`
|
|
354
|
+
} else {
|
|
355
|
+
const scale = Math.min((slideW * 0.25) / dim.w, (slideH * 0.3333) / dim.h)
|
|
356
|
+
const wPct = ((dim.w * scale / slideW) * 100).toFixed(2)
|
|
357
|
+
const hPct = ((dim.h * scale / slideH) * 100).toFixed(2)
|
|
358
|
+
return `url("${url}") left top / ${wPct}% ${hPct}% repeat`
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return `url("${url}") left top / 25% auto repeat`
|
|
362
|
+
}
|
|
363
|
+
return `url("${url}") center center / 100% 100% no-repeat`
|
|
364
|
+
}
|
|
90
365
|
|
|
91
366
|
// 背景类型与颜色
|
|
92
367
|
const bgFillType = computed({
|
|
@@ -102,13 +377,37 @@ const bgFillType = computed({
|
|
|
102
377
|
if (activeSlide.value) {
|
|
103
378
|
activeSlide.value.bgType = val
|
|
104
379
|
if (val === 'color') {
|
|
105
|
-
if (typeof activeSlide.value.background === 'string' && (activeSlide.value.background.startsWith('linear-gradient') || activeSlide.value.background.startsWith('radial-gradient') || activeSlide.value.background.
|
|
380
|
+
if (typeof activeSlide.value.background === 'string' && (activeSlide.value.background.startsWith('linear-gradient') || activeSlide.value.background.startsWith('radial-gradient') || activeSlide.value.background.includes('url('))) {
|
|
106
381
|
activeSlide.value.background = '#ffffff'
|
|
107
382
|
}
|
|
108
383
|
} else if (val === 'gradient') {
|
|
109
|
-
activeSlide.value.background
|
|
384
|
+
if (typeof activeSlide.value.background !== 'string' || !activeSlide.value.background.includes('gradient')) {
|
|
385
|
+
activeSlide.value.background = 'linear-gradient(135deg, #f43f5e 0%, #f97316 100%)'
|
|
386
|
+
}
|
|
110
387
|
} else if (val === 'image') {
|
|
111
|
-
activeSlide.value.background
|
|
388
|
+
const existingUrl = extractUrlFromBgCss(activeSlide.value.background)
|
|
389
|
+
const isExistingSvgPattern = existingUrl.startsWith('data:image/svg+xml')
|
|
390
|
+
if (activeBgTab.value === 'image') {
|
|
391
|
+
const targetUrl = (!isExistingSvgPattern && existingUrl) ? existingUrl : ((activeSlide.value as any)._lastBgImageUrl || '')
|
|
392
|
+
; (activeSlide.value as any).isPattern = false
|
|
393
|
+
if (targetUrl) {
|
|
394
|
+
const mode = (activeSlide.value as any).bgImageMode || 'stretch'
|
|
395
|
+
activeSlide.value.background = formatSlideBgImageCss(targetUrl, mode)
|
|
396
|
+
} else {
|
|
397
|
+
activeSlide.value.background = ''
|
|
398
|
+
}
|
|
399
|
+
} else if (activeBgTab.value === 'pattern') {
|
|
400
|
+
; (activeSlide.value as any).isPattern = true
|
|
401
|
+
if (isExistingSvgPattern && existingUrl) {
|
|
402
|
+
activeSlide.value.background = `url("${existingUrl}") left top / auto repeat`
|
|
403
|
+
} else {
|
|
404
|
+
const pType = (activeSlide.value as any).bgPatternType || 'dots-sparse'
|
|
405
|
+
const pFg = (activeSlide.value as any).bgPatternFg || '#3B82F6'
|
|
406
|
+
const pBg = (activeSlide.value as any).bgPatternBg || '#FFFFFF'
|
|
407
|
+
const patUrl = buildPatternSvgDataUrl(pType, pFg, pBg)
|
|
408
|
+
activeSlide.value.background = `url("${patUrl}") left top / auto repeat`
|
|
409
|
+
}
|
|
410
|
+
}
|
|
112
411
|
}
|
|
113
412
|
addHistorySnapshot()
|
|
114
413
|
}
|
|
@@ -140,21 +439,78 @@ const activeBgColor = computed({
|
|
|
140
439
|
return '#ffffff'
|
|
141
440
|
},
|
|
142
441
|
set(val: string) {
|
|
442
|
+
onSelectBgColor(val)
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const activeBgHex = computed(() => {
|
|
447
|
+
return parseColor(activeBgColor.value).hex || '#FFFFFF'
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const displayBgOpacity = computed(() => {
|
|
451
|
+
const bg = activeSlide.value?.background
|
|
452
|
+
if (!bg) return 100
|
|
453
|
+
const parsed = parseColor(bg)
|
|
454
|
+
return parsed.alpha !== undefined ? Math.max(0, Math.min(100, Math.round(parsed.alpha))) : 100
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
function handleBgOpacityInput(e: Event) {
|
|
458
|
+
const target = e.target as HTMLInputElement
|
|
459
|
+
let val = parseInt(target.value)
|
|
460
|
+
if (isNaN(val)) val = 100
|
|
461
|
+
val = Math.max(0, Math.min(100, val))
|
|
462
|
+
setBgColorWithOpacity(activeBgHex.value, val)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function onSelectBgColor(color: string) {
|
|
466
|
+
if (!color || color === 'transparent' || color === 'none') {
|
|
143
467
|
if (activeSlide.value) {
|
|
144
|
-
activeSlide.value.background =
|
|
468
|
+
activeSlide.value.background = '#ffffff'
|
|
145
469
|
activeSlide.value.bgType = 'color'
|
|
146
470
|
addHistorySnapshot()
|
|
147
471
|
}
|
|
472
|
+
return
|
|
148
473
|
}
|
|
149
|
-
|
|
474
|
+
const parsed = parseColor(color)
|
|
475
|
+
const alpha = (parsed.alpha !== undefined && parsed.alpha < 100) ? parsed.alpha : displayBgOpacity.value
|
|
476
|
+
setBgColorWithOpacity(parsed.hex, alpha)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function setBgColorWithOpacity(hexColor: string, alpha: number) {
|
|
480
|
+
if (!activeSlide.value) return
|
|
481
|
+
const cleanHex = hexColor.startsWith('#') ? hexColor.slice(0, 7) : `#${hexColor.slice(0, 6)}`
|
|
482
|
+
if (alpha >= 100) {
|
|
483
|
+
activeSlide.value.background = cleanHex
|
|
484
|
+
} else {
|
|
485
|
+
const alphaHex = Math.round(alpha * 255 / 100).toString(16).padStart(2, '0').toUpperCase()
|
|
486
|
+
activeSlide.value.background = `${cleanHex}${alphaHex}`
|
|
487
|
+
}
|
|
488
|
+
activeSlide.value.bgType = 'color'
|
|
489
|
+
addHistorySnapshot()
|
|
490
|
+
}
|
|
150
491
|
|
|
151
492
|
const bgImageMode = computed({
|
|
152
493
|
get() {
|
|
153
|
-
|
|
494
|
+
if ((activeSlide.value as any)?.bgImageMode) {
|
|
495
|
+
return (activeSlide.value as any).bgImageMode
|
|
496
|
+
}
|
|
497
|
+
const bg = activeSlide.value?.background
|
|
498
|
+
if (typeof bg === 'string' && bg.includes('url(')) {
|
|
499
|
+
if (/\brepeat\b/i.test(bg) && !/\bno-repeat\b/i.test(bg)) return 'tile'
|
|
500
|
+
if (/\bcontain\b/i.test(bg)) return 'contain'
|
|
501
|
+
if (/\bcover\b/i.test(bg) || /\bauto\s+no-repeat\b/i.test(bg)) return 'cover'
|
|
502
|
+
}
|
|
503
|
+
return 'stretch'
|
|
154
504
|
},
|
|
155
505
|
set(val: string) {
|
|
156
506
|
if (activeSlide.value) {
|
|
157
|
-
(activeSlide.value as any).bgImageMode = val
|
|
507
|
+
; (activeSlide.value as any).bgImageMode = val
|
|
508
|
+
const currentUrl = bgImageUrl.value || (activeSlide.value as any)._lastBgImageUrl || ''
|
|
509
|
+
if (currentUrl) {
|
|
510
|
+
; (activeSlide.value as any).isPattern = false
|
|
511
|
+
activeSlide.value.background = formatSlideBgImageCss(currentUrl, val)
|
|
512
|
+
activeSlide.value.bgType = 'image'
|
|
513
|
+
}
|
|
158
514
|
addHistorySnapshot()
|
|
159
515
|
}
|
|
160
516
|
}
|
|
@@ -162,17 +518,32 @@ const bgImageMode = computed({
|
|
|
162
518
|
|
|
163
519
|
const bgImageUrl = computed({
|
|
164
520
|
get() {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
521
|
+
const rawUrl = extractUrlFromBgCss(activeSlide.value?.background)
|
|
522
|
+
if (activeBgTab.value === 'pattern') {
|
|
523
|
+
return rawUrl
|
|
524
|
+
}
|
|
525
|
+
if (rawUrl && !rawUrl.startsWith('data:image/svg+xml')) {
|
|
526
|
+
return rawUrl
|
|
527
|
+
}
|
|
528
|
+
return (activeSlide.value as any)?._lastBgImageUrl || rawUrl || ''
|
|
168
529
|
},
|
|
169
530
|
set(url: string) {
|
|
170
531
|
if (activeSlide.value) {
|
|
171
532
|
if (!url) {
|
|
172
|
-
activeSlide.value.
|
|
173
|
-
|
|
533
|
+
; (activeSlide.value as any)._lastBgImageUrl = ''
|
|
534
|
+
; (activeSlide.value as any).isPattern = false
|
|
535
|
+
activeSlide.value.background = ''
|
|
536
|
+
activeSlide.value.bgType = 'image'
|
|
174
537
|
} else {
|
|
175
|
-
|
|
538
|
+
const isSvgPattern = url.startsWith('data:image/svg+xml')
|
|
539
|
+
; (activeSlide.value as any).isPattern = isSvgPattern
|
|
540
|
+
if (!isSvgPattern) {
|
|
541
|
+
; (activeSlide.value as any)._lastBgImageUrl = url
|
|
542
|
+
const mode = bgImageMode.value || 'stretch'
|
|
543
|
+
activeSlide.value.background = formatSlideBgImageCss(url, mode)
|
|
544
|
+
} else {
|
|
545
|
+
activeSlide.value.background = `url("${url}") left top / auto repeat`
|
|
546
|
+
}
|
|
176
547
|
activeSlide.value.bgType = 'image'
|
|
177
548
|
}
|
|
178
549
|
addHistorySnapshot()
|
|
@@ -180,6 +551,89 @@ const bgImageUrl = computed({
|
|
|
180
551
|
}
|
|
181
552
|
})
|
|
182
553
|
|
|
554
|
+
const bgImagePreviewStyle = computed(() => {
|
|
555
|
+
const url = bgImageUrl.value
|
|
556
|
+
if (!url) return {}
|
|
557
|
+
const css = formatSlideBgImageCss(url, bgImageMode.value)
|
|
558
|
+
return {
|
|
559
|
+
background: `transparent ${css}`
|
|
560
|
+
}
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const activeBgPatternMeta = computed(() => {
|
|
564
|
+
const rawUrl = extractUrlFromBgCss(activeSlide.value?.background)
|
|
565
|
+
return parsePatternSvgMeta(rawUrl)
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
const activeBgPatternType = computed(() => {
|
|
569
|
+
return (activeSlide.value as any)?.bgPatternType || activeBgPatternMeta.value.type || 'dots-sparse'
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
const activeBgPatternFg = computed(() => {
|
|
573
|
+
return (activeSlide.value as any)?.bgPatternFg || activeBgPatternMeta.value.fg || '#3B82F6'
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const activeBgPatternBg = computed(() => {
|
|
577
|
+
return (activeSlide.value as any)?.bgPatternBg || activeBgPatternMeta.value.bg || '#FFFFFF'
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
const parsedBgPatternFg = computed(() => parseColor(activeBgPatternFg.value))
|
|
581
|
+
const parsedBgPatternBg = computed(() => parseColor(activeBgPatternBg.value))
|
|
582
|
+
|
|
583
|
+
function getBgPatternPresetPreviewUrl(p: { key: string; url: string }) {
|
|
584
|
+
return buildPatternSvgDataUrl(p.key, activeBgPatternFg.value, activeBgPatternBg.value)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isBgPatternPresetActive(p: { key: string; url: string }) {
|
|
588
|
+
return activeBgPatternType.value === p.key
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function applyBgPattern(type: string, fg: string, bg: string) {
|
|
592
|
+
if (!activeSlide.value) return
|
|
593
|
+
;(activeSlide.value as any).bgPatternType = type
|
|
594
|
+
;(activeSlide.value as any).bgPatternFg = fg
|
|
595
|
+
;(activeSlide.value as any).bgPatternBg = bg
|
|
596
|
+
;(activeSlide.value as any).isPattern = true
|
|
597
|
+
const patUrl = buildPatternSvgDataUrl(type, fg, bg)
|
|
598
|
+
activeSlide.value.background = `url("${patUrl}") left top / auto repeat`
|
|
599
|
+
activeSlide.value.bgType = 'image'
|
|
600
|
+
addHistorySnapshot()
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function selectBgPatternPreset(p: { key: string; url: string }) {
|
|
604
|
+
applyBgPattern(p.key, activeBgPatternFg.value, activeBgPatternBg.value)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function onSelectBgPatternFgColor(color: string) {
|
|
608
|
+
const fg = (!color || color === 'transparent' || color === 'none') ? 'transparent' : color
|
|
609
|
+
applyBgPattern(activeBgPatternType.value, fg, activeBgPatternBg.value)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function handleBgPatternFgOpacityInput(e: Event) {
|
|
613
|
+
const target = e.target as HTMLInputElement
|
|
614
|
+
let val = parseInt(target.value)
|
|
615
|
+
if (isNaN(val)) val = 100
|
|
616
|
+
val = Math.max(0, Math.min(100, val))
|
|
617
|
+
const hex = parsedBgPatternFg.value.hex === 'transparent' ? '#3B82F6' : parsedBgPatternFg.value.hex
|
|
618
|
+
const fg = val === 0 ? 'transparent' : (val === 100 ? hex : hexToRgba(hex, val))
|
|
619
|
+
applyBgPattern(activeBgPatternType.value, fg, activeBgPatternBg.value)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function onSelectBgPatternBgColor(color: string) {
|
|
623
|
+
const bg = (!color || color === 'transparent' || color === 'none') ? 'transparent' : color
|
|
624
|
+
applyBgPattern(activeBgPatternType.value, activeBgPatternFg.value, bg)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function handleBgPatternBgOpacityInput(e: Event) {
|
|
628
|
+
const target = e.target as HTMLInputElement
|
|
629
|
+
let val = parseInt(target.value)
|
|
630
|
+
if (isNaN(val)) val = 100
|
|
631
|
+
val = Math.max(0, Math.min(100, val))
|
|
632
|
+
const hex = parsedBgPatternBg.value.hex === 'transparent' ? '#FFFFFF' : parsedBgPatternBg.value.hex
|
|
633
|
+
const bg = val === 0 ? 'transparent' : (val === 100 ? hex : hexToRgba(hex, val))
|
|
634
|
+
applyBgPattern(activeBgPatternType.value, activeBgPatternFg.value, bg)
|
|
635
|
+
}
|
|
636
|
+
|
|
183
637
|
const bgFileInputRef = ref<HTMLInputElement | null>(null)
|
|
184
638
|
const uploadBgImage = () => {
|
|
185
639
|
bgFileInputRef.value?.click()
|
|
@@ -215,6 +669,82 @@ const applyBackground = (css: string) => {
|
|
|
215
669
|
}
|
|
216
670
|
}
|
|
217
671
|
|
|
672
|
+
type BgFillMode = 'solid' | 'gradient-linear' | 'gradient-radial' | 'gradient-rect' | 'image' | 'pattern'
|
|
673
|
+
|
|
674
|
+
const activeBgTab = ref<BgFillMode>('solid')
|
|
675
|
+
|
|
676
|
+
const isSolidBg = computed(() => activeBgTab.value === 'solid' || (activeBgTab.value as string) === 'color')
|
|
677
|
+
const isGradientBg = computed(() => activeBgTab.value.startsWith('gradient'))
|
|
678
|
+
const isImageBg = computed(() => activeBgTab.value === 'image')
|
|
679
|
+
const isPatternBg = computed(() => activeBgTab.value === 'pattern')
|
|
680
|
+
|
|
681
|
+
const bgGradientEditorRef = ref<any>(null)
|
|
682
|
+
|
|
683
|
+
watch(
|
|
684
|
+
[() => activeSlide.value?.bgType, () => activeSlide.value?.background, () => activeIndex.value],
|
|
685
|
+
() => {
|
|
686
|
+
const bg = activeSlide.value?.background
|
|
687
|
+
const effectiveType = activeSlide.value?.bgType || (bg ? normalizeSlideBackground(bg).bgType : 'color')
|
|
688
|
+
if (effectiveType === 'image' || (typeof bg === 'string' && bg.includes('url('))) {
|
|
689
|
+
const url = extractUrlFromBgCss(bg)
|
|
690
|
+
if (url && !url.startsWith('data:image/svg+xml') && activeSlide.value) {
|
|
691
|
+
; (activeSlide.value as any)._lastBgImageUrl = url
|
|
692
|
+
ensureBgImageSizeLoaded(url)
|
|
693
|
+
if (!(activeSlide.value as any)?.isPattern) {
|
|
694
|
+
const formatted = formatSlideBgImageCss(url, bgImageMode.value)
|
|
695
|
+
if (typeof bg === 'string' && bg !== formatted) {
|
|
696
|
+
activeSlide.value.background = formatted
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if ((typeof bg === 'string' && bg.includes('data:image/svg+xml')) || (activeSlide.value as any)?.isPattern) {
|
|
701
|
+
activeBgTab.value = 'pattern'
|
|
702
|
+
} else {
|
|
703
|
+
activeBgTab.value = 'image'
|
|
704
|
+
}
|
|
705
|
+
} else if (effectiveType === 'gradient' || (typeof bg === 'string' && bg.includes('gradient('))) {
|
|
706
|
+
if (typeof bg === 'string') {
|
|
707
|
+
if (bg.includes('closest-side') || bg.includes('rect') || bg.includes('ellipse')) activeBgTab.value = 'gradient-rect'
|
|
708
|
+
else if (bg.includes('radial') || bg.includes('circle')) activeBgTab.value = 'gradient-radial'
|
|
709
|
+
else activeBgTab.value = 'gradient-linear'
|
|
710
|
+
} else {
|
|
711
|
+
activeBgTab.value = 'gradient-linear'
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
if (!(activeBgTab.value === 'image' && activeSlide.value?.bgType === 'image' && !bg)) {
|
|
715
|
+
activeBgTab.value = 'solid'
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
{ immediate: true }
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
function selectBgTab(tab: BgFillMode) {
|
|
723
|
+
activeBgTab.value = tab
|
|
724
|
+
if (tab === 'solid') {
|
|
725
|
+
bgFillType.value = 'color'
|
|
726
|
+
} else if (tab === 'image') {
|
|
727
|
+
bgFillType.value = 'image'
|
|
728
|
+
} else if (tab === 'pattern') {
|
|
729
|
+
bgFillType.value = 'image'
|
|
730
|
+
} else if (tab.startsWith('gradient-')) {
|
|
731
|
+
bgFillType.value = 'gradient'
|
|
732
|
+
const gType = tab.replace('gradient-', '') as 'linear' | 'radial' | 'rect'
|
|
733
|
+
if (gType === 'linear') {
|
|
734
|
+
applyBackground('linear-gradient(135deg, #f8fafc 0%, #cbd5e1 100%)')
|
|
735
|
+
} else if (gType === 'radial') {
|
|
736
|
+
applyBackground('radial-gradient(circle farthest-corner at 50% 50%, #f8fafc 0%, #cbd5e1 100%)')
|
|
737
|
+
} else if (gType === 'rect') {
|
|
738
|
+
applyBackground('radial-gradient(closest-side at 50% 50%, #ffffff 0%, #cbd5e1 100%)')
|
|
739
|
+
}
|
|
740
|
+
nextTick(() => {
|
|
741
|
+
if (bgGradientEditorRef.value?.changeType) {
|
|
742
|
+
bgGradientEditorRef.value.changeType(gType)
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
218
748
|
const activeGradientCss = computed(() => {
|
|
219
749
|
const bg = activeSlide.value?.background
|
|
220
750
|
if (!bg) return 'linear-gradient(135deg, #f43f5e 0%, #f97316 100%)'
|
|
@@ -229,6 +759,36 @@ const activeGradientCss = computed(() => {
|
|
|
229
759
|
return 'linear-gradient(135deg, #f43f5e 0%, #f97316 100%)'
|
|
230
760
|
})
|
|
231
761
|
|
|
762
|
+
const activeGradientStart = computed(() => {
|
|
763
|
+
const bg = activeSlide.value?.background
|
|
764
|
+
if (typeof bg === 'string' && bg.includes('gradient')) {
|
|
765
|
+
const match = bg.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)/g)
|
|
766
|
+
if (match && match.length > 0) return parseColor(match[0]).hex
|
|
767
|
+
}
|
|
768
|
+
return '#3b82f6'
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
const activeGradientEnd = computed(() => {
|
|
772
|
+
const bg = activeSlide.value?.background
|
|
773
|
+
if (typeof bg === 'string' && bg.includes('gradient')) {
|
|
774
|
+
const match = bg.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)/g)
|
|
775
|
+
if (match && match.length > 1) return parseColor(match[match.length - 1]).hex
|
|
776
|
+
}
|
|
777
|
+
return generateGradientEndColor(activeGradientStart.value)
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
const activeGradientMid = computed(() => {
|
|
781
|
+
const bg = activeSlide.value?.background
|
|
782
|
+
if (typeof bg === 'string' && bg.includes('gradient')) {
|
|
783
|
+
const match = bg.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)/g)
|
|
784
|
+
if (match && match.length > 2) {
|
|
785
|
+
const midIdx = Math.floor(match.length / 2)
|
|
786
|
+
return parseColor(match[midIdx]).hex
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return '#cbd5e1'
|
|
790
|
+
})
|
|
791
|
+
|
|
232
792
|
const onGradientEditorUpdate = (css: string) => {
|
|
233
793
|
if (activeSlide.value) {
|
|
234
794
|
activeSlide.value.background = css
|
|
@@ -391,351 +951,441 @@ watch(extractTab, () => {
|
|
|
391
951
|
<input ref="bgFileInputRef" type="file" accept="image/*" style="display: none;" @change="onBgFileChange" />
|
|
392
952
|
|
|
393
953
|
<!-- 1. 全局设置 Tab (Global Settings) -->
|
|
394
|
-
<div v-if="designActiveTab === 'global'"
|
|
954
|
+
<div v-if="designActiveTab === 'global'" class="global-tab-content">
|
|
395
955
|
<!-- 画布尺寸 -->
|
|
396
|
-
<div class="
|
|
397
|
-
<
|
|
398
|
-
<el-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
956
|
+
<div class="global-form-item">
|
|
957
|
+
<div class="global-item-label">画布尺寸</div>
|
|
958
|
+
<el-popover v-model:visible="isRatioPopoverVisible" placement="bottom-start" :width="ratioTriggerWidth"
|
|
959
|
+
trigger="click" popper-class="autofit-popover-panel" :show-arrow="false" :offset="4"
|
|
960
|
+
@before-enter="onRatioPopoverShow">
|
|
961
|
+
<template #reference>
|
|
962
|
+
<div ref="ratioTriggerRef" class="custom-select-trigger" :class="{ 'is-active': isRatioPopoverVisible }">
|
|
963
|
+
<span class="select-trigger-text">{{ currentRatioLabel }}</span>
|
|
964
|
+
<svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
|
|
965
|
+
:class="{ 'is-open': isRatioPopoverVisible }">
|
|
966
|
+
<path
|
|
967
|
+
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">
|
|
968
|
+
</path>
|
|
969
|
+
</svg>
|
|
970
|
+
</div>
|
|
971
|
+
</template>
|
|
972
|
+
|
|
973
|
+
<div class="autofit-menu-container">
|
|
974
|
+
<div v-for="opt in ratioOptions" :key="opt.value" class="autofit-menu-item"
|
|
975
|
+
:class="{ 'is-selected': slideRatio === opt.value }" @click="selectRatio(opt.value)">
|
|
976
|
+
<span class="autofit-item-label">{{ opt.label }}</span>
|
|
977
|
+
<svg v-if="slideRatio === opt.value" viewBox="0 0 24 24" width="15" height="15" fill="none"
|
|
978
|
+
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
|
|
979
|
+
class="autofit-check-icon">
|
|
980
|
+
<polyline points="20 6 9 17 4 12" />
|
|
981
|
+
</svg>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
</el-popover>
|
|
407
985
|
</div>
|
|
408
986
|
|
|
987
|
+
<!-- 分割线 -->
|
|
988
|
+
<div class="global-divider"></div>
|
|
989
|
+
|
|
409
990
|
<!-- 字体 -->
|
|
410
|
-
<div class="
|
|
411
|
-
<
|
|
412
|
-
<el-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
<
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
991
|
+
<div class="global-form-item">
|
|
992
|
+
<div class="global-item-label">字体</div>
|
|
993
|
+
<el-popover v-model:visible="isFontPopoverVisible" placement="bottom-start" :width="fontTriggerWidth"
|
|
994
|
+
trigger="click" popper-class="autofit-popover-panel font-select-popover" :show-arrow="false" :offset="4"
|
|
995
|
+
@before-enter="onFontPopoverShow">
|
|
996
|
+
<template #reference>
|
|
997
|
+
<div ref="fontTriggerRef" class="custom-select-trigger" :class="{ 'is-active': isFontPopoverVisible }">
|
|
998
|
+
<span class="select-trigger-text"
|
|
999
|
+
:style="{ fontFamily: (globalFontFamily && globalFontFamily !== '默认字体') ? globalFontFamily : undefined }">
|
|
1000
|
+
{{ currentFontFamilyLabel }}
|
|
1001
|
+
</span>
|
|
1002
|
+
<svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
|
|
1003
|
+
:class="{ 'is-open': isFontPopoverVisible }">
|
|
1004
|
+
<path
|
|
1005
|
+
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">
|
|
1006
|
+
</path>
|
|
1007
|
+
</svg>
|
|
1008
|
+
</div>
|
|
1009
|
+
</template>
|
|
1010
|
+
|
|
1011
|
+
<div class="autofit-menu-container font-menu-scrollable">
|
|
1012
|
+
<div class="autofit-menu-item" :class="{ 'is-selected': !globalFontFamily || globalFontFamily === '默认字体' }"
|
|
1013
|
+
@click="selectFont('默认字体')">
|
|
1014
|
+
<span class="autofit-item-label">默认字体</span>
|
|
1015
|
+
<svg v-if="!globalFontFamily || globalFontFamily === '默认字体'" viewBox="0 0 24 24" width="15" height="15"
|
|
1016
|
+
fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
|
|
1017
|
+
class="autofit-check-icon">
|
|
1018
|
+
<polyline points="20 6 9 17 4 12" />
|
|
1019
|
+
</svg>
|
|
1020
|
+
</div>
|
|
1021
|
+
<div v-for="font in SYS_FONTS" :key="font.value" class="autofit-menu-item"
|
|
1022
|
+
:class="{ 'is-selected': globalFontFamily === font.value }" @click="selectFont(font.value)">
|
|
1023
|
+
<span class="autofit-item-label" :style="{ fontFamily: font.value }">{{ font.label }}</span>
|
|
1024
|
+
<svg v-if="globalFontFamily === font.value" viewBox="0 0 24 24" width="15" height="15" fill="none"
|
|
1025
|
+
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
|
|
1026
|
+
class="autofit-check-icon">
|
|
1027
|
+
<polyline points="20 6 9 17 4 12" />
|
|
1028
|
+
</svg>
|
|
1029
|
+
</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
</el-popover>
|
|
419
1032
|
</div>
|
|
420
1033
|
|
|
421
1034
|
<!-- 字体颜色 -->
|
|
422
|
-
<div class="
|
|
423
|
-
<
|
|
1035
|
+
<div class="global-form-item" style="margin-top: 14px;">
|
|
1036
|
+
<div class="global-item-label">字体颜色</div>
|
|
424
1037
|
<QuickColorPopover v-model:visible="globalFontColorPopoverVisible" :color="globalFontColor"
|
|
425
1038
|
v-model:recentColors="quickRecentColors" @select-color="(color) => globalFontColor = color">
|
|
426
1039
|
<template #reference>
|
|
427
|
-
<div class="color-
|
|
428
|
-
<div class="color-pill-
|
|
429
|
-
<div :style="{
|
|
430
|
-
<span
|
|
431
|
-
{{ formatHexDisplay(globalFontColor) }}
|
|
432
|
-
</span>
|
|
1040
|
+
<div class="global-color-pill">
|
|
1041
|
+
<div class="color-pill-left">
|
|
1042
|
+
<div class="color-swatch" :style="{ backgroundColor: globalFontColor }"></div>
|
|
1043
|
+
<span class="color-hex-text">{{ formatHexDisplay(globalFontColor) }}</span>
|
|
433
1044
|
</div>
|
|
1045
|
+
<div class="color-opacity-text">100%</div>
|
|
434
1046
|
</div>
|
|
435
1047
|
</template>
|
|
436
1048
|
</QuickColorPopover>
|
|
1049
|
+
|
|
1050
|
+
<button class="global-action-btn" style="margin-top: 10px;" @click="applyFontToAll">
|
|
1051
|
+
全局统一字体
|
|
1052
|
+
</button>
|
|
437
1053
|
</div>
|
|
438
1054
|
|
|
439
|
-
<!--
|
|
440
|
-
<div
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1055
|
+
<!-- 分割线 -->
|
|
1056
|
+
<div class="global-divider"></div>
|
|
1057
|
+
|
|
1058
|
+
<!-- 背景色 -->
|
|
1059
|
+
<div class="global-form-item">
|
|
1060
|
+
<div class="global-item-label">背景色</div>
|
|
1061
|
+
|
|
1062
|
+
<!-- 7 个填充类型切换按钮 -->
|
|
1063
|
+
<div class="fill-types-bar">
|
|
1064
|
+
<button type="button" class="fill-type-icon-btn" :class="{ active: isSolidBg }" @click="selectBgTab('solid')"
|
|
1065
|
+
title="纯色填充">
|
|
1066
|
+
<svg viewBox="0 0 18 18" width="18" height="18">
|
|
1067
|
+
<rect x="0.5" y="0.5" width="17" height="17" rx="2"
|
|
1068
|
+
:fill="isSolidBg ? (activeBgColor && activeBgColor !== 'transparent' ? activeBgColor : '#3b82f6') : '#cbd5e1'"
|
|
1069
|
+
:stroke="isSolidBg ? '#1E6FFF' : '#94a3b8'" stroke-width="1" />
|
|
1070
|
+
</svg>
|
|
1071
|
+
</button>
|
|
1072
|
+
<button type="button" class="fill-type-icon-btn" :class="{ active: activeBgTab === 'gradient-linear' }"
|
|
1073
|
+
@click="selectBgTab('gradient-linear')" title="线性渐变">
|
|
1074
|
+
<svg viewBox="0 0 18 18" width="18" height="18">
|
|
1075
|
+
<defs>
|
|
1076
|
+
<linearGradient id="bgFillBarLinear" x1="0" y1="0" x2="0" y2="1">
|
|
1077
|
+
<stop offset="0%" :stop-color="activeBgTab === 'gradient-linear' ? activeGradientStart : '#94a3b8'" />
|
|
1078
|
+
<stop offset="100%" :stop-color="activeBgTab === 'gradient-linear' ? activeGradientEnd : '#ffffff'" />
|
|
1079
|
+
</linearGradient>
|
|
1080
|
+
</defs>
|
|
1081
|
+
<rect x="0.5" y="0.5" width="17" height="17" rx="2" fill="url(#bgFillBarLinear)"
|
|
1082
|
+
:stroke="activeBgTab === 'gradient-linear' ? '#1E6FFF' : '#94a3b8'" stroke-width="1" />
|
|
1083
|
+
</svg>
|
|
1084
|
+
</button>
|
|
1085
|
+
<button type="button" class="fill-type-icon-btn" :class="{ active: activeBgTab === 'gradient-radial' }"
|
|
1086
|
+
@click="selectBgTab('gradient-radial')" title="径向渐变">
|
|
1087
|
+
<svg viewBox="0 0 18 18" width="18" height="18">
|
|
1088
|
+
<defs>
|
|
1089
|
+
<radialGradient id="bgFillBarRadial" cx="50%" cy="50%" r="50%">
|
|
1090
|
+
<stop offset="15%" :stop-color="activeBgTab === 'gradient-radial' ? activeGradientEnd : '#ffffff'" />
|
|
1091
|
+
<stop offset="100%"
|
|
1092
|
+
:stop-color="activeBgTab === 'gradient-radial' ? activeGradientStart : '#cbd5e1'" />
|
|
1093
|
+
</radialGradient>
|
|
1094
|
+
</defs>
|
|
1095
|
+
<rect x="0.5" y="0.5" width="17" height="17" rx="2" fill="url(#bgFillBarRadial)"
|
|
1096
|
+
:stroke="activeBgTab === 'gradient-radial' ? '#1E6FFF' : '#94a3b8'" stroke-width="1" />
|
|
1097
|
+
</svg>
|
|
1098
|
+
</button>
|
|
1099
|
+
<button type="button" class="fill-type-icon-btn" :class="{ active: activeBgTab === 'gradient-rect' }"
|
|
1100
|
+
@click="selectBgTab('gradient-rect')" title="菱形渐变">
|
|
1101
|
+
<svg viewBox="0 0 18 18" width="18" height="18">
|
|
1102
|
+
<defs>
|
|
1103
|
+
<radialGradient id="bgFillBarRect" cx="50%" cy="50%" r="50%">
|
|
1104
|
+
<stop offset="0%" :stop-color="activeBgTab === 'gradient-rect' ? activeGradientEnd : '#ffffff'" />
|
|
1105
|
+
<stop offset="60%" :stop-color="activeBgTab === 'gradient-rect' ? activeGradientMid : '#e2e8f0'" />
|
|
1106
|
+
<stop offset="100%" :stop-color="activeBgTab === 'gradient-rect' ? activeGradientStart : '#94a3b8'" />
|
|
1107
|
+
</radialGradient>
|
|
1108
|
+
</defs>
|
|
1109
|
+
<rect x="0.5" y="0.5" width="17" height="17" rx="2" fill="url(#bgFillBarRect)"
|
|
1110
|
+
:stroke="activeBgTab === 'gradient-rect' ? '#1E6FFF' : '#94a3b8'" stroke-width="1" />
|
|
1111
|
+
</svg>
|
|
1112
|
+
</button>
|
|
1113
|
+
<button type="button" class="fill-type-icon-btn" :class="{ active: activeBgTab === 'image' }"
|
|
1114
|
+
@click="selectBgTab('image')" title="图片填充">
|
|
1115
|
+
<svg viewBox="0 0 18 18" width="18" height="18" fill="none">
|
|
1116
|
+
<rect x="0.5" y="0.5" width="17" height="17" rx="2"
|
|
1117
|
+
:stroke="activeBgTab === 'image' ? '#1E6FFF' : '#94a3b8'" stroke-width="1" fill="#ffffff" />
|
|
1118
|
+
<circle cx="6" cy="6" r="1.5" :fill="activeBgTab === 'image' ? '#1E6FFF' : '#94a3b8'" />
|
|
1119
|
+
<path d="M2.5 14L7 9.5L10.5 13L12.5 11L15.5 14" :stroke="activeBgTab === 'image' ? '#1E6FFF' : '#94a3b8'"
|
|
1120
|
+
stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round" />
|
|
1121
|
+
</svg>
|
|
1122
|
+
</button>
|
|
1123
|
+
<button type="button" class="fill-type-icon-btn" :class="{ active: activeBgTab === 'pattern' }"
|
|
1124
|
+
@click="selectBgTab('pattern')" title="图案填充">
|
|
1125
|
+
<svg viewBox="0 0 18 18" width="18" height="18">
|
|
1126
|
+
<defs>
|
|
1127
|
+
<pattern id="bgFillBarPattern" width="4" height="4" patternUnits="userSpaceOnUse"
|
|
1128
|
+
patternTransform="rotate(45)">
|
|
1129
|
+
<line x1="0" y1="0" x2="0" y2="4" :stroke="activeBgTab === 'pattern' ? '#1E6FFF' : '#94a3b8'"
|
|
1130
|
+
stroke-width="1" />
|
|
1131
|
+
</pattern>
|
|
1132
|
+
</defs>
|
|
1133
|
+
<rect x="0.5" y="0.5" width="17" height="17" rx="2" fill="url(#bgFillBarPattern)"
|
|
1134
|
+
:stroke="activeBgTab === 'pattern' ? '#1E6FFF' : '#94a3b8'" stroke-width="1" />
|
|
1135
|
+
</svg>
|
|
1136
|
+
</button>
|
|
465
1137
|
</div>
|
|
466
|
-
</div>
|
|
467
1138
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
<div
|
|
481
|
-
|
|
482
|
-
|
|
1139
|
+
<!-- 纯色填充卡片 (对齐 SidebarShapeFormatPanel.vue) -->
|
|
1140
|
+
<template v-if="isSolidBg">
|
|
1141
|
+
<div class="full-color-pill">
|
|
1142
|
+
<QuickColorPopover v-model:visible="bgColorPopoverVisible" :color="activeBgHex" :alpha="displayBgOpacity"
|
|
1143
|
+
v-model:recentColors="quickRecentColors" @select-color="onSelectBgColor">
|
|
1144
|
+
<template #reference>
|
|
1145
|
+
<div class="color-left-info">
|
|
1146
|
+
<div class="color-square-preview" :style="{ backgroundColor: activeBgColor }"></div>
|
|
1147
|
+
<span class="color-hex-text">{{ formatHexDisplay(activeBgColor) }}</span>
|
|
1148
|
+
</div>
|
|
1149
|
+
</template>
|
|
1150
|
+
</QuickColorPopover>
|
|
1151
|
+
<div class="color-opacity-input-wrapper" @click.stop title="不透明度 (0%为完全透明,100%为完全不透明)">
|
|
1152
|
+
<input type="number" class="color-opacity-num-input" :value="displayBgOpacity" min="0" max="100"
|
|
1153
|
+
@input="handleBgOpacityInput" @keydown.enter="($event.target as HTMLElement).blur()" />
|
|
1154
|
+
<span class="percent-unit">%</span>
|
|
1155
|
+
</div>
|
|
483
1156
|
</div>
|
|
484
|
-
</
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1157
|
+
</template>
|
|
1158
|
+
|
|
1159
|
+
<!-- 渐变填充卡片 -->
|
|
1160
|
+
<template v-else-if="isGradientBg">
|
|
1161
|
+
<GradientEditor ref="bgGradientEditorRef" :hide-type-buttons="true" :model-value="activeGradientCss"
|
|
1162
|
+
:recent-colors="quickRecentColors" :theme-colors="globalThemeColor"
|
|
1163
|
+
@update:model-value="onGradientEditorUpdate" @change="onGradientEditorChange" />
|
|
1164
|
+
</template>
|
|
1165
|
+
|
|
1166
|
+
<!-- 图片填充卡片 (默认及选中态) -->
|
|
1167
|
+
<div v-else-if="isImageBg" class="global-image-card">
|
|
1168
|
+
<div class="image-mode-row">
|
|
1169
|
+
<el-popover v-model:visible="isImageModePopoverVisible" placement="bottom-start"
|
|
1170
|
+
:width="imageModeTriggerWidth" trigger="click" popper-class="autofit-popover-panel" :show-arrow="false"
|
|
1171
|
+
:offset="4" @before-enter="onImageModePopoverShow">
|
|
1172
|
+
<template #reference>
|
|
1173
|
+
<div ref="imageModeTriggerRef" class="custom-select-trigger image-mode-trigger"
|
|
1174
|
+
:class="{ 'is-active': isImageModePopoverVisible }">
|
|
1175
|
+
<span class="select-trigger-text">{{ currentImageModeLabel }}</span>
|
|
1176
|
+
<svg viewBox="0 0 1024 1024" width="10" height="10" fill="currentColor" class="select-trigger-arrow"
|
|
1177
|
+
:class="{ 'is-open': isImageModePopoverVisible }">
|
|
1178
|
+
<path
|
|
1179
|
+
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">
|
|
1180
|
+
</path>
|
|
1181
|
+
</svg>
|
|
1182
|
+
</div>
|
|
1183
|
+
</template>
|
|
1184
|
+
|
|
1185
|
+
<div class="autofit-menu-container">
|
|
1186
|
+
<div v-for="opt in imageModeOptions" :key="opt.value" class="autofit-menu-item"
|
|
1187
|
+
:class="{ 'is-selected': bgImageMode === opt.value }" @click="selectImageMode(opt.value)">
|
|
1188
|
+
<span class="autofit-item-label">{{ opt.label }}</span>
|
|
1189
|
+
<svg v-if="bgImageMode === opt.value" viewBox="0 0 24 24" width="15" height="15" fill="none"
|
|
1190
|
+
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
|
|
1191
|
+
class="autofit-check-icon">
|
|
1192
|
+
<polyline points="20 6 9 17 4 12" />
|
|
1193
|
+
</svg>
|
|
1194
|
+
</div>
|
|
1195
|
+
</div>
|
|
1196
|
+
</el-popover>
|
|
1197
|
+
|
|
1198
|
+
<button class="image-fit-btn" @click="bgImageMode = 'stretch'" title="铺满模式">
|
|
1199
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3"
|
|
1200
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
1201
|
+
<path d="M2 5V2h3M14 5V2h-3M2 11v3h3M14 11v3h-3" />
|
|
1202
|
+
</svg>
|
|
1203
|
+
</button>
|
|
1204
|
+
</div>
|
|
1205
|
+
|
|
1206
|
+
<!-- 棋盘格预览框 -->
|
|
1207
|
+
<div class="checkerboard-preview-box" @click="uploadBgImage">
|
|
501
1208
|
<template v-if="bgImageUrl">
|
|
502
|
-
<
|
|
503
|
-
<div class="bg-
|
|
504
|
-
|
|
505
|
-
@click
|
|
506
|
-
<button class="design-action-btn"
|
|
507
|
-
style="padding: 3px 8px; font-size: 11px; background: #fff; color: #333; border: none; border-radius: 4px; cursor: pointer;"
|
|
508
|
-
@click="uploadBgImage">更换</button>
|
|
509
|
-
<button class="design-action-btn"
|
|
510
|
-
style="padding: 3px 8px; font-size: 11px; background: #ef4444; color: #fff; border: none; border-radius: 4px; cursor: pointer;"
|
|
511
|
-
@click="bgImageUrl = ''">删除</button>
|
|
1209
|
+
<div class="bg-img-preview" :style="bgImagePreviewStyle"></div>
|
|
1210
|
+
<div class="bg-img-hover-actions" @click.stop>
|
|
1211
|
+
<button class="bg-img-action-btn" @click="uploadBgImage">更换图片</button>
|
|
1212
|
+
<button class="bg-img-action-btn btn-del" @click="bgImageUrl = ''">删除</button>
|
|
512
1213
|
</div>
|
|
513
1214
|
</template>
|
|
514
1215
|
<template v-else>
|
|
515
|
-
<
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
</div>
|
|
1216
|
+
<button class="select-img-btn" @click.stop="uploadBgImage">
|
|
1217
|
+
选择图片
|
|
1218
|
+
</button>
|
|
519
1219
|
</template>
|
|
520
1220
|
</div>
|
|
521
1221
|
</div>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1222
|
+
|
|
1223
|
+
<!-- 图案填充卡片 -->
|
|
1224
|
+
<template v-else-if="isPatternBg">
|
|
1225
|
+
<div class="pattern-fill-container">
|
|
1226
|
+
<div class="pattern-presets-grid">
|
|
1227
|
+
<div v-for="p in PATTERN_PRESETS" :key="p.key" class="pattern-preset-card"
|
|
1228
|
+
:class="{ active: isBgPatternPresetActive(p) }" :title="p.name"
|
|
1229
|
+
:style="{ backgroundImage: `url('${getBgPatternPresetPreviewUrl(p)}')` }"
|
|
1230
|
+
@click="selectBgPatternPreset(p)"></div>
|
|
1231
|
+
</div>
|
|
1232
|
+
|
|
1233
|
+
<!-- 前景 / 背景 颜色选择 (对齐 PPT / WPS 图案填充) -->
|
|
1234
|
+
<div class="pattern-colors-section">
|
|
1235
|
+
<div class="pattern-color-row">
|
|
1236
|
+
<span class="pattern-color-label">前景</span>
|
|
1237
|
+
<div class="full-color-pill" style="flex: 1;">
|
|
1238
|
+
<QuickColorPopover v-model:visible="bgPatternFgPopoverVisible"
|
|
1239
|
+
:color="parsedBgPatternFg.hex === 'transparent' ? '#3B82F6' : parsedBgPatternFg.hex"
|
|
1240
|
+
:alpha="parsedBgPatternFg.alpha" v-model:recentColors="quickRecentColors"
|
|
1241
|
+
@select-color="onSelectBgPatternFgColor">
|
|
1242
|
+
<template #reference>
|
|
1243
|
+
<div class="color-left-info">
|
|
1244
|
+
<div class="color-square-preview"
|
|
1245
|
+
:style="{ backgroundColor: parsedBgPatternFg.hex === 'transparent' ? 'transparent' : parsedBgPatternFg.hex }">
|
|
1246
|
+
</div>
|
|
1247
|
+
<span class="color-hex-text">{{ formatHexDisplay(parsedBgPatternFg.hex) }}</span>
|
|
1248
|
+
</div>
|
|
1249
|
+
</template>
|
|
1250
|
+
</QuickColorPopover>
|
|
1251
|
+
<div class="color-opacity-input-wrapper" @click.stop title="前景不透明度">
|
|
1252
|
+
<input type="number" class="color-opacity-num-input" :value="parsedBgPatternFg.alpha" min="0"
|
|
1253
|
+
max="100" @input="handleBgPatternFgOpacityInput"
|
|
1254
|
+
@keydown.enter="($event.target as HTMLElement).blur()" />
|
|
1255
|
+
<span class="percent-unit">%</span>
|
|
535
1256
|
</div>
|
|
536
1257
|
</div>
|
|
537
|
-
<span style="font-size: 10px; color: var(--text-muted);">▼</span>
|
|
538
1258
|
</div>
|
|
539
|
-
</template>
|
|
540
|
-
<div style="display: flex; flex-direction: column; gap: 10px; padding: 4px 0;">
|
|
541
|
-
<div v-for="cIdx in 5" :key="cIdx"
|
|
542
|
-
style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
|
543
|
-
<span style="font-size: 12px; color: var(--text-secondary); width: 64px;">色彩 {{ cIdx }}</span>
|
|
544
|
-
<QuickColorPopover v-model:visible="globalThemeColorPopoverVisible[cIdx - 1]"
|
|
545
|
-
:color="globalThemeColor[cIdx - 1]" v-model:recentColors="quickRecentColors" placement="left-start"
|
|
546
|
-
@select-color="(color) => globalThemeColor[cIdx - 1] = color"
|
|
547
|
-
@color-picker-show="isCustomColorPickerOpen = true"
|
|
548
|
-
@color-picker-hide="() => { isCustomColorPickerOpen = false }">
|
|
549
|
-
<template #reference>
|
|
550
|
-
<div class="color-picker-pill-wrapper" style="width: 100%;">
|
|
551
|
-
<div class="color-pill-inner" style="height: 28px;">
|
|
552
|
-
<div
|
|
553
|
-
style="width: 12px; height: 12px; border: 1px solid rgba(0,0,0,0.1); border-radius: 2px; flex-shrink: 0;"
|
|
554
|
-
:style="{ background: globalThemeColor[cIdx - 1] }"></div>
|
|
555
|
-
<span
|
|
556
|
-
style="font-size: 11px; color: var(--text-primary); font-family: monospace; font-weight: 500;">
|
|
557
|
-
{{ formatHexDisplay(globalThemeColor[cIdx - 1]) }}
|
|
558
|
-
</span>
|
|
559
|
-
</div>
|
|
560
|
-
</div>
|
|
561
|
-
</template>
|
|
562
|
-
</QuickColorPopover>
|
|
563
|
-
</div>
|
|
564
|
-
</div>
|
|
565
|
-
</el-popover>
|
|
566
|
-
</div>
|
|
567
|
-
</div>
|
|
568
1259
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1260
|
+
<div class="pattern-color-row">
|
|
1261
|
+
<span class="pattern-color-label">背景</span>
|
|
1262
|
+
<div class="full-color-pill" style="flex: 1;">
|
|
1263
|
+
<QuickColorPopover v-model:visible="bgPatternBgPopoverVisible"
|
|
1264
|
+
:color="parsedBgPatternBg.hex === 'transparent' ? '#FFFFFF' : parsedBgPatternBg.hex"
|
|
1265
|
+
:alpha="parsedBgPatternBg.alpha" v-model:recentColors="quickRecentColors"
|
|
1266
|
+
@select-color="onSelectBgPatternBgColor">
|
|
1267
|
+
<template #reference>
|
|
1268
|
+
<div class="color-left-info">
|
|
1269
|
+
<div class="color-square-preview"
|
|
1270
|
+
:style="{ backgroundColor: parsedBgPatternBg.hex === 'transparent' ? 'transparent' : parsedBgPatternBg.hex }">
|
|
1271
|
+
</div>
|
|
1272
|
+
<span class="color-hex-text">{{ formatHexDisplay(parsedBgPatternBg.hex) }}</span>
|
|
1273
|
+
</div>
|
|
1274
|
+
</template>
|
|
1275
|
+
</QuickColorPopover>
|
|
1276
|
+
<div class="color-opacity-input-wrapper" @click.stop title="背景不透明度">
|
|
1277
|
+
<input type="number" class="color-opacity-num-input" :value="parsedBgPatternBg.alpha" min="0"
|
|
1278
|
+
max="100" @input="handleBgPatternBgOpacityInput"
|
|
1279
|
+
@keydown.enter="($event.target as HTMLElement).blur()" />
|
|
1280
|
+
<span class="percent-unit">%</span>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
581
1283
|
</div>
|
|
582
1284
|
</div>
|
|
583
|
-
</template>
|
|
584
|
-
</QuickColorPopover>
|
|
585
|
-
</div>
|
|
586
|
-
|
|
587
|
-
<!-- 边框样式 -->
|
|
588
|
-
<div class="design-property-row">
|
|
589
|
-
<span class="shape-property-label">边框样式</span>
|
|
590
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: center; flex: 1;">
|
|
591
|
-
<el-select v-model="globalBorderStyle" size="small" style="flex: 1;" class="design-select"
|
|
592
|
-
placement="bottom-start" :fallback-placements="['bottom-start', 'bottom', 'top-start']">
|
|
593
|
-
<el-option value="solid" label="──────" />
|
|
594
|
-
<el-option value="dashed" label="------" />
|
|
595
|
-
<el-option value="dotted" label="······" />
|
|
596
|
-
<el-option value="none" label="无" />
|
|
597
|
-
</el-select>
|
|
598
|
-
<div class="shape-border-width-wrapper"
|
|
599
|
-
style="height: 32px; border: none; border-radius: 4px; padding: 0 8px; display: flex; align-items: center; box-sizing: border-box; background: #F5F6F7;">
|
|
600
|
-
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"
|
|
601
|
-
style="margin-right: 4px; color: var(--text-secondary); flex-shrink: 0;">
|
|
602
|
-
<line x1="3" y1="6" x2="21" y2="6" />
|
|
603
|
-
<line x1="3" y1="12" x2="21" y2="12" />
|
|
604
|
-
<line x1="3" y1="18" x2="21" y2="18" />
|
|
605
|
-
</svg>
|
|
606
|
-
<input type="number" v-model.number="globalBorderWidth" :min="0" :max="20" class="shape-num-field"
|
|
607
|
-
style="width: 100%; border: none; background: transparent; outline: none; text-align: left; font-size: 12px; color: var(--text-primary); -moz-appearance: textfield; appearance: none;" />
|
|
608
1285
|
</div>
|
|
609
|
-
</
|
|
610
|
-
</div>
|
|
1286
|
+
</template>
|
|
611
1287
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
<el-button type="primary" size="small" class="design-action-btn-primary"
|
|
617
|
-
@click="applyThemeToAll">主题色应用到全部</el-button>
|
|
618
|
-
<el-button type="primary" size="small" class="design-action-btn-secondary"
|
|
619
|
-
@click="applyFontToAll">全局统一字体</el-button>
|
|
620
|
-
<el-button type="primary" size="small" class="design-action-btn-secondary"
|
|
621
|
-
@click="openThemeExtractor">从幻灯片提取主题</el-button>
|
|
1288
|
+
<!-- 背景色应用到全部 按钮 -->
|
|
1289
|
+
<button class="global-action-btn" style="margin-top: 10px;" @click="applyBackgroundToAll">
|
|
1290
|
+
背景色应用到全部
|
|
1291
|
+
</button>
|
|
622
1292
|
</div>
|
|
623
1293
|
</div>
|
|
624
1294
|
|
|
625
1295
|
<!-- 2. 主题设置 Tab (Theme Settings) -->
|
|
626
|
-
<div v-else-if="designActiveTab === 'theme'"
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
:
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
<div class="
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1296
|
+
<div v-else-if="designActiveTab === 'theme'" class="theme-tab-content">
|
|
1297
|
+
<StyleBuilderDialog v-if="styleBuilderMounted" :visible="styleBuilderVisible"
|
|
1298
|
+
@update:visible="emit('update:styleBuilderVisible', $event)" :target-mode="styleBuilderTargetMode"
|
|
1299
|
+
:initial-recipe-id="styleBuilderInitialRecipeId" :is-new-custom="styleBuilderIsNewCustom" />
|
|
1300
|
+
|
|
1301
|
+
<div class="theme-card-list">
|
|
1302
|
+
<div v-for="recipe in builtinStyleRecipes" :key="recipe.id" class="theme-recipe-card"
|
|
1303
|
+
:class="{ 'is-active': currentActiveThemeId === recipe.id }" @click="handleSelectTheme(recipe)">
|
|
1304
|
+
<!-- Card Header: Title + More Actions Menu -->
|
|
1305
|
+
<div class="theme-card-header">
|
|
1306
|
+
<span class="theme-name-text">{{ recipe.name }}</span>
|
|
1307
|
+
<el-popover trigger="click" placement="left-start" popper-class="theme-actions-popper" :width="156"
|
|
1308
|
+
:offset="175" :show-arrow="false" :teleported="true">
|
|
1309
|
+
<template #reference>
|
|
1310
|
+
<button class="theme-more-btn" @click.stop title="更多选项">
|
|
1311
|
+
<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
|
|
1312
|
+
<circle cx="8" cy="3" r="1.5" />
|
|
1313
|
+
<circle cx="8" cy="8" r="1.5" />
|
|
1314
|
+
<circle cx="8" cy="13" r="1.5" />
|
|
1315
|
+
</svg>
|
|
1316
|
+
</button>
|
|
1317
|
+
</template>
|
|
643
1318
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
{{ recipe.name }}
|
|
654
|
-
</span>
|
|
655
|
-
<span v-if="currentActiveThemeId === recipe.id" class="current-slide-pill">
|
|
656
|
-
{{ isMasterEditingView ? '当前母版' : '当前页' }}
|
|
657
|
-
</span>
|
|
658
|
-
</div>
|
|
659
|
-
<div v-if="!isMasterEditingView" class="recipe-usage-badge" :class="getThemeUsageLabel(recipe.id).type">
|
|
660
|
-
{{ getThemeUsageLabel(recipe.id).text }}
|
|
661
|
-
</div>
|
|
662
|
-
</div>
|
|
1319
|
+
<div class="theme-menu-list" @click.stop>
|
|
1320
|
+
<div class="theme-menu-item" @click="handleEditTheme(recipe)">
|
|
1321
|
+
<svg class="menu-item-icon" viewBox="0 0 16 16" width="14" height="14" fill="none"
|
|
1322
|
+
stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
1323
|
+
<rect x="2" y="2" width="12" height="12" rx="2" />
|
|
1324
|
+
<path d="M5 5h6M5 8h4M5 11h2" />
|
|
1325
|
+
</svg>
|
|
1326
|
+
<span>编辑</span>
|
|
1327
|
+
</div>
|
|
663
1328
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1329
|
+
<div class="theme-menu-item" @click="handleDuplicateTheme(recipe)">
|
|
1330
|
+
<svg class="menu-item-icon" viewBox="0 0 16 16" width="14" height="14" fill="none"
|
|
1331
|
+
stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
1332
|
+
<rect x="5" y="5" width="9" height="9" rx="1.5" />
|
|
1333
|
+
<path d="M11 2H3.5A1.5 1.5 0 0 0 2 3.5V11" />
|
|
1334
|
+
</svg>
|
|
1335
|
+
<span>复制</span>
|
|
1336
|
+
</div>
|
|
1337
|
+
|
|
1338
|
+
<div class="theme-menu-item" @click="handleApplyToAllSlides(recipe)">
|
|
1339
|
+
<svg class="menu-item-icon" viewBox="0 0 16 16" width="14" height="14" fill="none"
|
|
1340
|
+
stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
1341
|
+
<rect x="2" y="2" width="12" height="12" rx="2" />
|
|
1342
|
+
<path d="M8 5v5M6 8l2 2 2-2" />
|
|
1343
|
+
</svg>
|
|
1344
|
+
<span>应用到所有幻灯片</span>
|
|
1345
|
+
</div>
|
|
1346
|
+
|
|
1347
|
+
<div class="theme-menu-item item-danger" @click="handleDeleteTheme(recipe)">
|
|
1348
|
+
<svg class="menu-item-icon" viewBox="0 0 16 16" width="14" height="14" fill="none"
|
|
1349
|
+
stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
1350
|
+
<path
|
|
1351
|
+
d="M3 4h10M6 4V2.5A1.5 1.5 0 0 1 7.5 1h1A1.5 1.5 0 0 1 10 2.5V4M4 4l.8 9.2A1.5 1.5 0 0 0 6.3 14.5h3.4a1.5 1.5 0 0 0 1.5-1.3L12 4" />
|
|
1352
|
+
</svg>
|
|
1353
|
+
<span>删除</span>
|
|
1354
|
+
</div>
|
|
1355
|
+
|
|
1356
|
+
<div class="theme-menu-divider"></div>
|
|
1357
|
+
|
|
1358
|
+
<div class="theme-menu-footer">
|
|
1359
|
+
用于 {{ getThemeSlideCount(recipe.id) }} 张幻灯片
|
|
1360
|
+
</div>
|
|
676
1361
|
</div>
|
|
1362
|
+
</el-popover>
|
|
1363
|
+
</div>
|
|
1364
|
+
|
|
1365
|
+
<!-- Card Inner Preview Box -->
|
|
1366
|
+
<div class="theme-preview-box" :style="{ backgroundColor: recipe.backgroundColor || '#f8fafc' }">
|
|
1367
|
+
<div class="theme-font-row">
|
|
1368
|
+
<span class="font-preview-aa" :style="{
|
|
1369
|
+
color: getThemeFontColors(recipe).aa,
|
|
1370
|
+
fontFamily: recipe.styleFonts?.titleFont || 'SourceHanSansCN'
|
|
1371
|
+
}">
|
|
1372
|
+
Aa
|
|
1373
|
+
</span>
|
|
1374
|
+
<span class="font-preview-name" :style="{
|
|
1375
|
+
color: getThemeFontColors(recipe).font,
|
|
1376
|
+
fontFamily: recipe.styleFonts?.titleFont || 'SourceHanSansCN'
|
|
1377
|
+
}">
|
|
1378
|
+
{{ fontLabel(recipe.styleFonts?.titleFont) }}
|
|
1379
|
+
</span>
|
|
677
1380
|
</div>
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
<button class="recipe-act-btn" @click.stop="emit('applyStyleRecipeToSlides', recipe, true)"
|
|
687
|
-
title="应用到所有母版">
|
|
688
|
-
🌐 全部母版
|
|
689
|
-
</button>
|
|
690
|
-
<button class="recipe-act-btn btn-edit" @click.stop="emit('openStyleBuilder', 'all', recipe.id)"
|
|
691
|
-
title="进入 Style Builder 编辑该主题">
|
|
692
|
-
✏️ 编辑
|
|
693
|
-
</button>
|
|
694
|
-
</template>
|
|
695
|
-
<template v-else>
|
|
696
|
-
<button class="recipe-act-btn" @click.stop="emit('applyStyleRecipeToSlides', recipe, false)"
|
|
697
|
-
:title="`仅应用到第 ${activeIndex + 1} 页`">
|
|
698
|
-
⚡ 仅当前页 (P{{ activeIndex + 1 }})
|
|
699
|
-
</button>
|
|
700
|
-
<button class="recipe-act-btn" @click.stop="emit('applyStyleRecipeToSlides', recipe, true)"
|
|
701
|
-
title="应用到全部幻灯片">
|
|
702
|
-
🌐 全部幻灯片
|
|
703
|
-
</button>
|
|
704
|
-
<button class="recipe-act-btn btn-edit" @click.stop="emit('openStyleBuilder', 'all', recipe.id)"
|
|
705
|
-
title="进入 Style Builder 编辑该主题">
|
|
706
|
-
✏️ 编辑
|
|
707
|
-
</button>
|
|
708
|
-
</template>
|
|
709
|
-
</div>
|
|
710
|
-
</div>
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
|
|
714
|
-
<!-- 预置主题列表 -->
|
|
715
|
-
<div v-if="presetThemes && presetThemes.length > 0" class="design-section" style="padding: 0; margin-top: 10px;">
|
|
716
|
-
<div class="section-title" style="margin-bottom: 12px; font-weight: bold; color: var(--text-primary);">预置配色主题
|
|
717
|
-
</div>
|
|
718
|
-
<div class="preset-themes-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
|
|
719
|
-
<div v-for="preset in presetThemes" :key="preset.name" class="preset-theme-card"
|
|
720
|
-
:style="{ background: preset.bgColor }" @click="applyPresetTheme(preset)"
|
|
721
|
-
style="width: 100%; box-sizing: border-box; padding: 10px 8px; border-radius: 8px; border: 1px solid var(--border-color); display: flex; flex-direction: column; gap: 6px; cursor: pointer;">
|
|
722
|
-
<div class="preset-theme-title" :style="{ fontFamily: preset.fontFamily, color: preset.fontColor }"
|
|
723
|
-
style="font-size: 13px; font-weight: bold; margin: 0; text-align: left;">
|
|
724
|
-
文字 Aa
|
|
725
|
-
</div>
|
|
726
|
-
<div class="preset-theme-palette"
|
|
727
|
-
style="display: flex; gap: 2px; border-radius: 2px; overflow: hidden; height: 8px;">
|
|
728
|
-
<div v-for="color in preset.palette" :key="color" :style="{ background: color, flex: 1, height: '100%' }">
|
|
729
|
-
</div>
|
|
730
|
-
</div>
|
|
731
|
-
<div class="preset-theme-name" :style="{ color: preset.isDark ? '#cbd5e1' : 'var(--text-muted)' }"
|
|
732
|
-
style="font-size: 10px; margin-top: auto; opacity: 0.8; text-align: left;">
|
|
733
|
-
{{ preset.name }}
|
|
734
|
-
</div>
|
|
735
|
-
</div>
|
|
736
|
-
</div>
|
|
737
|
-
</div>
|
|
738
|
-
</div>
|
|
1381
|
+
<div class="theme-palette-strip">
|
|
1382
|
+
<div v-for="(color, cIdx) in (recipe.styleColors || []).slice(0, 8)" :key="cIdx"
|
|
1383
|
+
class="palette-color-block" :style="{ backgroundColor: color }"></div>
|
|
1384
|
+
</div>
|
|
1385
|
+
</div>
|
|
1386
|
+
</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
</div>
|
|
739
1389
|
|
|
740
1390
|
<!-- 3. 版式设置 Tab (Layout Settings) -->
|
|
741
1391
|
<div v-else-if="designActiveTab === 'layout' && !isMasterEditingView"
|
|
@@ -748,7 +1398,9 @@ watch(extractTab, () => {
|
|
|
748
1398
|
|
|
749
1399
|
<div v-for="masterGroup in masterGroupedLayouts" :key="masterGroup.id" class="preset-layout-master-group"
|
|
750
1400
|
style="margin-bottom: 16px;">
|
|
751
|
-
<div
|
|
1401
|
+
<div
|
|
1402
|
+
style="font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 8px; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
|
1403
|
+
:title="masterGroup.name">
|
|
752
1404
|
{{ masterGroup.name }}
|
|
753
1405
|
</div>
|
|
754
1406
|
<div class="preset-layout-grid"
|
|
@@ -1172,156 +1824,729 @@ watch(extractTab, () => {
|
|
|
1172
1824
|
font-size: 12px;
|
|
1173
1825
|
}
|
|
1174
1826
|
|
|
1175
|
-
/*
|
|
1176
|
-
.
|
|
1177
|
-
|
|
1178
|
-
box-sizing: border-box;
|
|
1179
|
-
border-radius: 10px;
|
|
1180
|
-
border: 1.5px solid var(--border-color, #e2e8f0);
|
|
1181
|
-
padding: 12px;
|
|
1827
|
+
/* Global Tab New UI Styles (Matching Reference Screenshot) */
|
|
1828
|
+
.global-tab-content {
|
|
1829
|
+
padding: 16px 15px;
|
|
1182
1830
|
display: flex;
|
|
1183
1831
|
flex-direction: column;
|
|
1184
|
-
gap: 10px;
|
|
1185
|
-
transition: all 0.2s ease;
|
|
1186
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
1187
1832
|
}
|
|
1188
1833
|
|
|
1189
|
-
.
|
|
1190
|
-
|
|
1191
|
-
|
|
1834
|
+
.global-form-item {
|
|
1835
|
+
display: flex;
|
|
1836
|
+
flex-direction: column;
|
|
1837
|
+
gap: 6px;
|
|
1192
1838
|
}
|
|
1193
1839
|
|
|
1194
|
-
.
|
|
1195
|
-
|
|
1196
|
-
|
|
1840
|
+
.global-item-label {
|
|
1841
|
+
font-size: 13px;
|
|
1842
|
+
font-weight: 500;
|
|
1843
|
+
color: #374151;
|
|
1844
|
+
line-height: 1.2;
|
|
1197
1845
|
}
|
|
1198
1846
|
|
|
1199
|
-
.
|
|
1847
|
+
.global-select-full {
|
|
1848
|
+
width: 100%;
|
|
1849
|
+
|
|
1850
|
+
:deep(.el-select__wrapper) {
|
|
1851
|
+
background-color: #ffffff !important;
|
|
1852
|
+
border: 1px solid #e2e8f0 !important;
|
|
1853
|
+
border-radius: 6px !important;
|
|
1854
|
+
box-shadow: none !important;
|
|
1855
|
+
height: 32px !important;
|
|
1856
|
+
min-height: 32px !important;
|
|
1857
|
+
line-height: 32px !important;
|
|
1858
|
+
padding: 0 10px !important;
|
|
1859
|
+
transition: all 0.15s ease !important;
|
|
1860
|
+
|
|
1861
|
+
&:hover {
|
|
1862
|
+
border-color: #cbd5e1 !important;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
&.is-focused {
|
|
1866
|
+
border-color: #1e6fff !important;
|
|
1867
|
+
box-shadow: 0 0 0 2px rgba(30, 111, 255, 0.1) !important;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
:deep(.el-select__selected-item) {
|
|
1872
|
+
font-size: 13px !important;
|
|
1873
|
+
font-weight: 500 !important;
|
|
1874
|
+
color: #1e293b !important;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
.custom-select-trigger {
|
|
1200
1879
|
display: flex;
|
|
1201
1880
|
align-items: center;
|
|
1202
1881
|
justify-content: space-between;
|
|
1203
|
-
|
|
1882
|
+
width: 100%;
|
|
1883
|
+
height: 34px;
|
|
1884
|
+
padding: 0 10px;
|
|
1885
|
+
background-color: #ffffff;
|
|
1886
|
+
border: 1px solid #e2e8f0;
|
|
1887
|
+
border-radius: 6px;
|
|
1888
|
+
cursor: pointer;
|
|
1889
|
+
box-sizing: border-box;
|
|
1890
|
+
transition: all 0.15s ease;
|
|
1891
|
+
|
|
1892
|
+
&:hover {
|
|
1893
|
+
border-color: #cbd5e1;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
&.is-active {
|
|
1897
|
+
border-color: #1e6fff;
|
|
1898
|
+
box-shadow: 0 0 0 2px rgba(30, 111, 255, 0.1);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
&.image-mode-trigger {
|
|
1902
|
+
flex: 1;
|
|
1903
|
+
}
|
|
1204
1904
|
}
|
|
1205
1905
|
|
|
1206
|
-
.
|
|
1906
|
+
.select-trigger-text {
|
|
1207
1907
|
font-size: 13px;
|
|
1208
|
-
font-weight:
|
|
1209
|
-
|
|
1210
|
-
white-space: nowrap;
|
|
1211
|
-
overflow: hidden;
|
|
1212
|
-
text-overflow: ellipsis;
|
|
1908
|
+
font-weight: 500;
|
|
1909
|
+
color: #1e293b;
|
|
1213
1910
|
}
|
|
1214
1911
|
|
|
1215
|
-
.
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
flex-shrink: 0;
|
|
1912
|
+
.select-trigger-arrow {
|
|
1913
|
+
color: #94a3b8;
|
|
1914
|
+
transition: transform 0.2s ease;
|
|
1915
|
+
|
|
1916
|
+
&.is-open {
|
|
1917
|
+
transform: rotate(180deg);
|
|
1918
|
+
}
|
|
1223
1919
|
}
|
|
1224
1920
|
|
|
1225
|
-
.
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1921
|
+
.autofit-menu-container {
|
|
1922
|
+
display: flex;
|
|
1923
|
+
flex-direction: column;
|
|
1924
|
+
gap: 2px;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
.font-menu-scrollable {
|
|
1928
|
+
max-height: 274px;
|
|
1929
|
+
overflow-y: auto;
|
|
1930
|
+
padding-right: 2px;
|
|
1931
|
+
|
|
1932
|
+
&::-webkit-scrollbar {
|
|
1933
|
+
width: 4px;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
&::-webkit-scrollbar-thumb {
|
|
1937
|
+
background-color: #cbd5e1;
|
|
1938
|
+
border-radius: 4px;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
.autofit-menu-item {
|
|
1943
|
+
display: flex;
|
|
1944
|
+
align-items: center;
|
|
1945
|
+
justify-content: space-between;
|
|
1946
|
+
height: 34px;
|
|
1947
|
+
min-height: 34px;
|
|
1948
|
+
line-height: 34px;
|
|
1230
1949
|
flex-shrink: 0;
|
|
1231
|
-
|
|
1950
|
+
box-sizing: border-box;
|
|
1951
|
+
padding: 0 10px;
|
|
1952
|
+
border-radius: 6px;
|
|
1953
|
+
cursor: pointer;
|
|
1954
|
+
transition: all 0.15s ease;
|
|
1955
|
+
user-select: none;
|
|
1956
|
+
|
|
1957
|
+
&:hover {
|
|
1958
|
+
background-color: #f8fafc;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
&.is-selected {
|
|
1962
|
+
background-color: #eef2ff;
|
|
1963
|
+
|
|
1964
|
+
.autofit-item-label {
|
|
1965
|
+
color: #1e6fff;
|
|
1966
|
+
font-weight: 500;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1232
1969
|
}
|
|
1233
1970
|
|
|
1234
|
-
.
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1971
|
+
.autofit-item-label {
|
|
1972
|
+
font-size: 13px;
|
|
1973
|
+
line-height: 34px;
|
|
1974
|
+
color: #1e293b;
|
|
1975
|
+
transition: color 0.15s ease;
|
|
1238
1976
|
}
|
|
1239
1977
|
|
|
1240
|
-
.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
border: 1px solid #bfdbfe;
|
|
1978
|
+
.autofit-check-icon {
|
|
1979
|
+
color: #1e6fff;
|
|
1980
|
+
flex-shrink: 0;
|
|
1244
1981
|
}
|
|
1245
1982
|
|
|
1246
|
-
.
|
|
1247
|
-
|
|
1248
|
-
color: #
|
|
1249
|
-
|
|
1983
|
+
.global-divider {
|
|
1984
|
+
height: 1px;
|
|
1985
|
+
background-color: #f1f5f9;
|
|
1986
|
+
margin: 14px 0;
|
|
1987
|
+
width: 100%;
|
|
1250
1988
|
}
|
|
1251
1989
|
|
|
1252
|
-
.
|
|
1990
|
+
.global-color-pill {
|
|
1991
|
+
width: 100%;
|
|
1992
|
+
box-sizing: border-box;
|
|
1993
|
+
height: 34px;
|
|
1994
|
+
border: 1px solid #e5e7eb;
|
|
1995
|
+
border-radius: 6px;
|
|
1996
|
+
background: #ffffff;
|
|
1997
|
+
padding: 0 10px;
|
|
1998
|
+
display: flex;
|
|
1999
|
+
align-items: center;
|
|
2000
|
+
justify-content: space-between;
|
|
1253
2001
|
cursor: pointer;
|
|
1254
|
-
|
|
1255
|
-
|
|
2002
|
+
transition: all 0.15s ease;
|
|
2003
|
+
|
|
2004
|
+
&:hover {
|
|
2005
|
+
border-color: #cbd5e1;
|
|
2006
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
.color-pill-left {
|
|
2010
|
+
display: flex;
|
|
2011
|
+
align-items: center;
|
|
2012
|
+
gap: 8px;
|
|
2013
|
+
|
|
2014
|
+
.color-swatch {
|
|
2015
|
+
width: 18px;
|
|
2016
|
+
height: 18px;
|
|
2017
|
+
border-radius: 4px;
|
|
2018
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
2019
|
+
flex-shrink: 0;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
.color-hex-text {
|
|
2023
|
+
font-size: 13px;
|
|
2024
|
+
font-weight: 500;
|
|
2025
|
+
color: #1f2937;
|
|
2026
|
+
font-family: inherit;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
.color-opacity-text {
|
|
2031
|
+
font-size: 13px;
|
|
2032
|
+
color: #4b5563;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
.global-action-btn {
|
|
2037
|
+
width: 100%;
|
|
2038
|
+
box-sizing: border-box;
|
|
2039
|
+
height: 34px;
|
|
2040
|
+
border: 1px solid #e5e7eb;
|
|
1256
2041
|
border-radius: 6px;
|
|
2042
|
+
background: #ffffff;
|
|
2043
|
+
color: #374151;
|
|
2044
|
+
font-size: 13px;
|
|
2045
|
+
font-weight: 500;
|
|
2046
|
+
cursor: pointer;
|
|
1257
2047
|
display: flex;
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
transition:
|
|
2048
|
+
align-items: center;
|
|
2049
|
+
justify-content: center;
|
|
2050
|
+
transition: all 0.15s ease;
|
|
2051
|
+
|
|
2052
|
+
&:hover {
|
|
2053
|
+
background: #f9fafb;
|
|
2054
|
+
border-color: #cbd5e1;
|
|
2055
|
+
color: #111827;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
&:active {
|
|
2059
|
+
background: #f3f4f6;
|
|
2060
|
+
}
|
|
1261
2061
|
}
|
|
1262
2062
|
|
|
1263
|
-
.
|
|
1264
|
-
|
|
2063
|
+
.fill-types-bar {
|
|
2064
|
+
display: flex;
|
|
2065
|
+
align-items: center;
|
|
2066
|
+
justify-content: space-between;
|
|
2067
|
+
margin-bottom: 12px;
|
|
2068
|
+
width: 100%;
|
|
2069
|
+
height: 40px;
|
|
2070
|
+
background: #f8f9fa;
|
|
2071
|
+
border: 1px solid #edf0f3;
|
|
2072
|
+
border-radius: 10px;
|
|
2073
|
+
padding: 3px 4px;
|
|
2074
|
+
box-sizing: border-box;
|
|
1265
2075
|
}
|
|
1266
2076
|
|
|
1267
|
-
.
|
|
2077
|
+
.fill-type-icon-btn {
|
|
2078
|
+
width: 32px;
|
|
2079
|
+
height: 32px;
|
|
2080
|
+
flex: 0 0 32px;
|
|
2081
|
+
border-radius: 9px;
|
|
2082
|
+
background: transparent;
|
|
2083
|
+
border: 1px solid transparent;
|
|
1268
2084
|
display: flex;
|
|
1269
|
-
align-items:
|
|
2085
|
+
align-items: center;
|
|
2086
|
+
justify-content: center;
|
|
2087
|
+
cursor: pointer;
|
|
2088
|
+
padding: 0;
|
|
2089
|
+
transition: all 0.15s ease;
|
|
2090
|
+
box-sizing: border-box;
|
|
2091
|
+
|
|
2092
|
+
&:hover:not(.active) {
|
|
2093
|
+
background: rgba(0, 0, 0, 0.04);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
&.active {
|
|
2097
|
+
width: 32px;
|
|
2098
|
+
height: 32px;
|
|
2099
|
+
background: #FFFFFF;
|
|
2100
|
+
border-radius: 9px;
|
|
2101
|
+
border: 1px solid #E5E7EB;
|
|
2102
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
2103
|
+
}
|
|
1270
2104
|
}
|
|
1271
2105
|
|
|
1272
|
-
.
|
|
2106
|
+
:deep(.color-span-box),
|
|
2107
|
+
:deep(.el-popover__reference-wrapper) {
|
|
2108
|
+
display: block !important;
|
|
2109
|
+
width: 100% !important;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
.full-color-pill {
|
|
2113
|
+
width: 100%;
|
|
2114
|
+
height: 34px;
|
|
2115
|
+
background: #f8f9fa;
|
|
2116
|
+
border: 1px solid #edf0f3;
|
|
2117
|
+
border-radius: 8px;
|
|
2118
|
+
padding: 0 10px;
|
|
1273
2119
|
display: flex;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
2120
|
+
align-items: center;
|
|
2121
|
+
justify-content: space-between;
|
|
2122
|
+
box-sizing: border-box;
|
|
2123
|
+
transition: all 0.15s ease;
|
|
2124
|
+
|
|
2125
|
+
&:hover {
|
|
2126
|
+
background: #f1f5f9;
|
|
2127
|
+
border-color: #e2e8f0;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
:deep(.color-span-box) {
|
|
2131
|
+
display: flex !important;
|
|
2132
|
+
align-items: center !important;
|
|
2133
|
+
flex: 1 !important;
|
|
2134
|
+
height: 100% !important;
|
|
2135
|
+
cursor: pointer;
|
|
2136
|
+
}
|
|
1278
2137
|
}
|
|
1279
2138
|
|
|
1280
|
-
.
|
|
2139
|
+
.color-left-info {
|
|
2140
|
+
display: flex;
|
|
2141
|
+
align-items: center;
|
|
2142
|
+
gap: 8px;
|
|
1281
2143
|
flex: 1;
|
|
1282
2144
|
height: 100%;
|
|
2145
|
+
cursor: pointer;
|
|
1283
2146
|
}
|
|
1284
2147
|
|
|
1285
|
-
.
|
|
2148
|
+
.color-square-preview {
|
|
2149
|
+
width: 16px;
|
|
2150
|
+
height: 16px;
|
|
2151
|
+
border-radius: 3px;
|
|
2152
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
2153
|
+
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
.color-hex-text {
|
|
2157
|
+
font-size: 13px;
|
|
2158
|
+
font-weight: 500;
|
|
2159
|
+
font-family: monospace, sans-serif;
|
|
2160
|
+
color: #1e293b;
|
|
2161
|
+
letter-spacing: 0.5px;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
.color-opacity-input-wrapper {
|
|
1286
2165
|
display: flex;
|
|
1287
|
-
gap: 6px;
|
|
1288
2166
|
align-items: center;
|
|
2167
|
+
justify-content: flex-end;
|
|
2168
|
+
height: 24px;
|
|
2169
|
+
padding: 0 4px;
|
|
2170
|
+
border-radius: 4px;
|
|
2171
|
+
transition: all 0.15s ease;
|
|
2172
|
+
background: transparent;
|
|
2173
|
+
|
|
2174
|
+
&:hover {
|
|
2175
|
+
background: rgba(0, 0, 0, 0.04);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
&:focus-within {
|
|
2179
|
+
background: #ffffff;
|
|
2180
|
+
box-shadow: 0 0 0 1px #1E6FFF;
|
|
2181
|
+
}
|
|
1289
2182
|
}
|
|
1290
2183
|
|
|
1291
|
-
.
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
color: #334155;
|
|
1298
|
-
font-size: 11px;
|
|
2184
|
+
.color-opacity-num-input {
|
|
2185
|
+
width: 26px;
|
|
2186
|
+
border: none;
|
|
2187
|
+
background: transparent;
|
|
2188
|
+
outline: none;
|
|
2189
|
+
font-size: 12px;
|
|
1299
2190
|
font-weight: 500;
|
|
2191
|
+
color: #475569;
|
|
2192
|
+
text-align: right;
|
|
2193
|
+
padding: 0 1px 0 0;
|
|
2194
|
+
font-family: inherit;
|
|
2195
|
+
|
|
2196
|
+
&::-webkit-outer-spin-button,
|
|
2197
|
+
&::-webkit-inner-spin-button {
|
|
2198
|
+
-webkit-appearance: none;
|
|
2199
|
+
margin: 0;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
.percent-unit {
|
|
2204
|
+
font-size: 12px;
|
|
2205
|
+
font-weight: 500;
|
|
2206
|
+
color: #64748b;
|
|
2207
|
+
user-select: none;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
.pattern-fill-container {
|
|
2211
|
+
display: flex;
|
|
2212
|
+
flex-direction: column;
|
|
2213
|
+
gap: 10px;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
.pattern-colors-section {
|
|
2217
|
+
display: flex;
|
|
2218
|
+
flex-direction: column;
|
|
2219
|
+
gap: 8px;
|
|
2220
|
+
padding-top: 2px;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
.pattern-color-row {
|
|
2224
|
+
display: flex;
|
|
2225
|
+
align-items: center;
|
|
2226
|
+
gap: 8px;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
.pattern-color-label {
|
|
2230
|
+
width: 36px;
|
|
2231
|
+
font-size: 13px;
|
|
2232
|
+
color: #475569;
|
|
2233
|
+
flex-shrink: 0;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
.pattern-presets-grid {
|
|
2237
|
+
display: grid;
|
|
2238
|
+
grid-template-columns: repeat(4, 1fr);
|
|
2239
|
+
gap: 8px;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
.pattern-preset-card {
|
|
2243
|
+
height: 38px;
|
|
2244
|
+
border-radius: 6px;
|
|
2245
|
+
border: 1px solid #e2e8f0;
|
|
2246
|
+
background-repeat: repeat;
|
|
1300
2247
|
cursor: pointer;
|
|
1301
|
-
white-space: nowrap;
|
|
1302
2248
|
transition: all 0.15s ease;
|
|
2249
|
+
|
|
2250
|
+
&:hover {
|
|
2251
|
+
border-color: #94a3b8;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
&.active {
|
|
2255
|
+
border-color: #1E6FFF;
|
|
2256
|
+
box-shadow: 0 0 0 1px #1E6FFF;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
.global-image-card {
|
|
2261
|
+
width: 100%;
|
|
2262
|
+
box-sizing: border-box;
|
|
2263
|
+
border: 1px solid #e5e7eb;
|
|
2264
|
+
border-radius: 8px;
|
|
2265
|
+
padding: 10px 12px;
|
|
2266
|
+
background: #ffffff;
|
|
2267
|
+
display: flex;
|
|
2268
|
+
flex-direction: column;
|
|
2269
|
+
gap: 10px;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
.image-mode-row {
|
|
2273
|
+
display: flex;
|
|
2274
|
+
align-items: center;
|
|
2275
|
+
gap: 8px;
|
|
2276
|
+
|
|
2277
|
+
.image-mode-select {
|
|
2278
|
+
flex: 1;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
.image-fit-btn {
|
|
2282
|
+
width: 34px;
|
|
2283
|
+
height: 34px;
|
|
2284
|
+
display: flex;
|
|
2285
|
+
align-items: center;
|
|
2286
|
+
justify-content: center;
|
|
2287
|
+
border: 1px solid #e5e7eb;
|
|
2288
|
+
border-radius: 6px;
|
|
2289
|
+
background: #ffffff;
|
|
2290
|
+
color: #6b7280;
|
|
2291
|
+
cursor: pointer;
|
|
2292
|
+
padding: 0;
|
|
2293
|
+
transition: all 0.15s ease;
|
|
2294
|
+
|
|
2295
|
+
&:hover {
|
|
2296
|
+
background: #f9fafb;
|
|
2297
|
+
border-color: #cbd5e1;
|
|
2298
|
+
color: #111827;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
.checkerboard-preview-box {
|
|
2304
|
+
width: 100%;
|
|
2305
|
+
height: 120px;
|
|
2306
|
+
box-sizing: border-box;
|
|
2307
|
+
border-radius: 6px;
|
|
2308
|
+
display: flex;
|
|
2309
|
+
align-items: center;
|
|
2310
|
+
justify-content: center;
|
|
2311
|
+
position: relative;
|
|
2312
|
+
overflow: hidden;
|
|
2313
|
+
cursor: pointer;
|
|
2314
|
+
background-color: #ffffff;
|
|
2315
|
+
background-image:
|
|
2316
|
+
linear-gradient(45deg, #e5e7eb 25%, transparent 25%),
|
|
2317
|
+
linear-gradient(-45deg, #e5e7eb 25%, transparent 25%),
|
|
2318
|
+
linear-gradient(45deg, transparent 75%, #e5e7eb 75%),
|
|
2319
|
+
linear-gradient(-45deg, transparent 75%, #e5e7eb 75%);
|
|
2320
|
+
background-size: 16px 16px;
|
|
2321
|
+
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
|
|
2322
|
+
|
|
2323
|
+
.select-img-btn {
|
|
2324
|
+
padding: 7px 16px;
|
|
2325
|
+
background: rgba(255, 255, 255, 0.9);
|
|
2326
|
+
backdrop-filter: blur(4px);
|
|
2327
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
2328
|
+
border-radius: 4px;
|
|
2329
|
+
font-size: 12px;
|
|
2330
|
+
font-weight: 500;
|
|
2331
|
+
color: #374151;
|
|
2332
|
+
cursor: pointer;
|
|
2333
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
2334
|
+
transition: all 0.15s ease;
|
|
2335
|
+
|
|
2336
|
+
&:hover {
|
|
2337
|
+
background: #ffffff;
|
|
2338
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
.bg-img-preview {
|
|
2343
|
+
width: 100%;
|
|
2344
|
+
height: 100%;
|
|
2345
|
+
pointer-events: none;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
.bg-img-hover-actions {
|
|
2349
|
+
position: absolute;
|
|
2350
|
+
inset: 0;
|
|
2351
|
+
background: rgba(0, 0, 0, 0.45);
|
|
2352
|
+
display: none;
|
|
2353
|
+
align-items: center;
|
|
2354
|
+
justify-content: center;
|
|
2355
|
+
gap: 8px;
|
|
2356
|
+
|
|
2357
|
+
.bg-img-action-btn {
|
|
2358
|
+
padding: 4px 10px;
|
|
2359
|
+
font-size: 11px;
|
|
2360
|
+
background: #ffffff;
|
|
2361
|
+
color: #334155;
|
|
2362
|
+
border: none;
|
|
2363
|
+
border-radius: 4px;
|
|
2364
|
+
cursor: pointer;
|
|
2365
|
+
|
|
2366
|
+
&.btn-del {
|
|
2367
|
+
background: #ef4444;
|
|
2368
|
+
color: #ffffff;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
&:hover .bg-img-hover-actions {
|
|
2374
|
+
display: flex;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/* Theme Tab New UI Styles (Matching Reference Screenshot) */
|
|
2379
|
+
.theme-tab-content {
|
|
2380
|
+
padding: 14px 15px;
|
|
2381
|
+
display: flex;
|
|
2382
|
+
flex-direction: column;
|
|
2383
|
+
gap: 12px;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
.theme-card-list {
|
|
2387
|
+
display: flex;
|
|
2388
|
+
flex-direction: column;
|
|
2389
|
+
gap: 12px;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
.theme-recipe-card {
|
|
2393
|
+
width: 100%;
|
|
2394
|
+
box-sizing: border-box;
|
|
2395
|
+
background: #ffffff;
|
|
2396
|
+
border: 1px solid #e5e7eb;
|
|
2397
|
+
border-radius: 8px;
|
|
2398
|
+
padding: 10px 12px 12px 12px;
|
|
2399
|
+
cursor: pointer;
|
|
2400
|
+
transition: all 0.2s ease;
|
|
2401
|
+
display: flex;
|
|
2402
|
+
flex-direction: column;
|
|
2403
|
+
gap: 8px;
|
|
2404
|
+
user-select: none;
|
|
2405
|
+
|
|
2406
|
+
&:hover {
|
|
2407
|
+
border-color: #cbd5e1;
|
|
2408
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
&.is-active {
|
|
2412
|
+
border: 1.5px solid #2970ff;
|
|
2413
|
+
box-shadow: 0 0 0 1px rgba(41, 112, 255, 0.08);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
.theme-card-header {
|
|
2418
|
+
display: flex;
|
|
2419
|
+
align-items: center;
|
|
2420
|
+
justify-content: space-between;
|
|
2421
|
+
line-height: 1.2;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
.theme-name-text {
|
|
2425
|
+
font-size: 13px;
|
|
2426
|
+
font-weight: 600;
|
|
2427
|
+
color: #1f2937;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
.theme-more-btn {
|
|
1303
2431
|
display: flex;
|
|
1304
2432
|
align-items: center;
|
|
1305
2433
|
justify-content: center;
|
|
2434
|
+
width: 20px;
|
|
2435
|
+
height: 20px;
|
|
2436
|
+
border: none;
|
|
2437
|
+
background: transparent;
|
|
2438
|
+
color: #6b7280;
|
|
2439
|
+
border-radius: 4px;
|
|
2440
|
+
cursor: pointer;
|
|
2441
|
+
padding: 0;
|
|
2442
|
+
transition: all 0.15s ease;
|
|
2443
|
+
|
|
2444
|
+
&:hover {
|
|
2445
|
+
background: #f3f4f6;
|
|
2446
|
+
color: #111827;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
.theme-preview-box {
|
|
2451
|
+
width: 100%;
|
|
2452
|
+
box-sizing: border-box;
|
|
2453
|
+
border-radius: 6px;
|
|
2454
|
+
padding: 9px 10px;
|
|
2455
|
+
display: flex;
|
|
2456
|
+
flex-direction: column;
|
|
2457
|
+
gap: 8px;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
.theme-font-row {
|
|
2461
|
+
display: flex;
|
|
2462
|
+
align-items: baseline;
|
|
2463
|
+
gap: 8px;
|
|
2464
|
+
|
|
2465
|
+
.font-preview-aa {
|
|
2466
|
+
font-size: 15px;
|
|
2467
|
+
font-weight: 700;
|
|
2468
|
+
font-style: italic;
|
|
2469
|
+
line-height: 1;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
.font-preview-name {
|
|
2473
|
+
font-size: 12px;
|
|
2474
|
+
line-height: 1;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
.theme-palette-strip {
|
|
2479
|
+
display: flex;
|
|
2480
|
+
width: 100%;
|
|
2481
|
+
height: 12px;
|
|
2482
|
+
border-radius: 2px;
|
|
2483
|
+
overflow: hidden;
|
|
2484
|
+
|
|
2485
|
+
.palette-color-block {
|
|
2486
|
+
flex: 1;
|
|
2487
|
+
height: 100%;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
:global(.theme-actions-popper.el-popover.el-popper) {
|
|
2492
|
+
padding: 4px 0 !important;
|
|
2493
|
+
border-radius: 8px !important;
|
|
2494
|
+
border: 1px solid #eaecf0 !important;
|
|
2495
|
+
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04) !important;
|
|
2496
|
+
min-width: 146px !important;
|
|
1306
2497
|
}
|
|
1307
2498
|
|
|
1308
|
-
.
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
2499
|
+
.theme-menu-list {
|
|
2500
|
+
display: flex;
|
|
2501
|
+
flex-direction: column;
|
|
2502
|
+
user-select: none;
|
|
1312
2503
|
}
|
|
1313
2504
|
|
|
1314
|
-
.
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
2505
|
+
.theme-menu-item {
|
|
2506
|
+
display: flex;
|
|
2507
|
+
align-items: center;
|
|
2508
|
+
gap: 8px;
|
|
2509
|
+
padding: 7px 14px;
|
|
2510
|
+
font-size: 13px;
|
|
2511
|
+
color: #374151;
|
|
2512
|
+
cursor: pointer;
|
|
2513
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
2514
|
+
|
|
2515
|
+
.menu-item-icon {
|
|
2516
|
+
width: 14px;
|
|
2517
|
+
height: 14px;
|
|
2518
|
+
flex-shrink: 0;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
&:hover {
|
|
2522
|
+
background-color: #f3f4f6;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
&.item-danger {
|
|
2526
|
+
color: #ef4444;
|
|
2527
|
+
|
|
2528
|
+
.menu-item-icon {
|
|
2529
|
+
color: #ef4444;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
&:hover {
|
|
2533
|
+
background-color: #fef2f2;
|
|
2534
|
+
color: #dc2626;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
.theme-menu-divider {
|
|
2540
|
+
height: 1px;
|
|
2541
|
+
background-color: #f3f4f6;
|
|
2542
|
+
margin: 4px 0;
|
|
1320
2543
|
}
|
|
1321
2544
|
|
|
1322
|
-
.
|
|
1323
|
-
|
|
1324
|
-
|
|
2545
|
+
.theme-menu-footer {
|
|
2546
|
+
font-size: 11px;
|
|
2547
|
+
color: #9ca3af;
|
|
2548
|
+
text-align: center;
|
|
2549
|
+
padding: 6px 12px;
|
|
1325
2550
|
}
|
|
1326
2551
|
|
|
1327
2552
|
:deep(.color-span-box) {
|
|
@@ -1339,3 +2564,116 @@ watch(extractTab, () => {
|
|
|
1339
2564
|
}
|
|
1340
2565
|
}
|
|
1341
2566
|
</style>
|
|
2567
|
+
|
|
2568
|
+
<style lang="scss">
|
|
2569
|
+
.autofit-popover-panel {
|
|
2570
|
+
min-width: 0 !important;
|
|
2571
|
+
padding: 6px !important;
|
|
2572
|
+
border-radius: 10px !important;
|
|
2573
|
+
border: 1px solid #edf0f3 !important;
|
|
2574
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
|
|
2575
|
+
background: #ffffff !important;
|
|
2576
|
+
z-index: 9999 !important;
|
|
2577
|
+
|
|
2578
|
+
.el-popper__arrow {
|
|
2579
|
+
display: none !important;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
.autofit-menu-item {
|
|
2583
|
+
height: 34px !important;
|
|
2584
|
+
min-height: 34px !important;
|
|
2585
|
+
line-height: 34px !important;
|
|
2586
|
+
flex-shrink: 0 !important;
|
|
2587
|
+
box-sizing: border-box !important;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
.el-popper.sidebar-select-popper {
|
|
2592
|
+
min-width: 0 !important;
|
|
2593
|
+
padding: 0 !important;
|
|
2594
|
+
border-radius: 10px !important;
|
|
2595
|
+
border: 1px solid #edf0f3 !important;
|
|
2596
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
|
|
2597
|
+
background: #ffffff !important;
|
|
2598
|
+
overflow: hidden !important;
|
|
2599
|
+
z-index: 9999 !important;
|
|
2600
|
+
|
|
2601
|
+
.el-popper__arrow {
|
|
2602
|
+
display: none !important;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
.el-select-dropdown {
|
|
2606
|
+
padding: 6px !important;
|
|
2607
|
+
margin: 0 !important;
|
|
2608
|
+
border: none !important;
|
|
2609
|
+
box-shadow: none !important;
|
|
2610
|
+
border-radius: 0 !important;
|
|
2611
|
+
background: transparent !important;
|
|
2612
|
+
box-sizing: border-box !important;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
.el-select-dropdown__wrap {
|
|
2616
|
+
max-height: 274px !important;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
.el-select-dropdown__list {
|
|
2620
|
+
padding: 0 !important;
|
|
2621
|
+
margin: 0 !important;
|
|
2622
|
+
display: flex !important;
|
|
2623
|
+
flex-direction: column !important;
|
|
2624
|
+
gap: 2px !important;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
.el-select-dropdown__item {
|
|
2628
|
+
height: 34px !important;
|
|
2629
|
+
min-height: 34px !important;
|
|
2630
|
+
line-height: 34px !important;
|
|
2631
|
+
flex-shrink: 0 !important;
|
|
2632
|
+
box-sizing: border-box !important;
|
|
2633
|
+
padding: 0 10px !important;
|
|
2634
|
+
border-radius: 6px !important;
|
|
2635
|
+
font-size: 13px !important;
|
|
2636
|
+
color: #1e293b !important;
|
|
2637
|
+
display: flex !important;
|
|
2638
|
+
align-items: center !important;
|
|
2639
|
+
justify-content: space-between !important;
|
|
2640
|
+
transition: all 0.15s ease !important;
|
|
2641
|
+
user-select: none !important;
|
|
2642
|
+
|
|
2643
|
+
&:hover,
|
|
2644
|
+
&.hover,
|
|
2645
|
+
&.is-hovering {
|
|
2646
|
+
background-color: #f8fafc !important;
|
|
2647
|
+
color: #1e293b !important;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
&.is-selected {
|
|
2651
|
+
background-color: #eef2ff !important;
|
|
2652
|
+
color: #2563eb !important;
|
|
2653
|
+
font-weight: 500 !important;
|
|
2654
|
+
|
|
2655
|
+
&::after {
|
|
2656
|
+
content: '';
|
|
2657
|
+
display: inline-block;
|
|
2658
|
+
width: 15px;
|
|
2659
|
+
height: 15px;
|
|
2660
|
+
flex-shrink: 0;
|
|
2661
|
+
margin-left: 8px;
|
|
2662
|
+
background-color: #2563eb;
|
|
2663
|
+
-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;
|
|
2664
|
+
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;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
.el-select-dropdown.sidebar-select-popper {
|
|
2671
|
+
border: none !important;
|
|
2672
|
+
box-shadow: none !important;
|
|
2673
|
+
border-radius: 0 !important;
|
|
2674
|
+
background: transparent !important;
|
|
2675
|
+
padding: 6px !important;
|
|
2676
|
+
margin: 0 !important;
|
|
2677
|
+
box-sizing: border-box !important;
|
|
2678
|
+
}
|
|
2679
|
+
</style>
|