jvs-ppt-ui 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/lib/check_imgs.html +18 -0
  2. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.css +187 -27
  3. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.js +1 -1
  4. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.json +345 -65
  5. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.ttf +0 -0
  6. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff +0 -0
  7. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff2 +0 -0
  8. package/dist/lib/jvs-ppt-ui/img/al.png +0 -0
  9. package/dist/lib/jvs-ppt-ui/img/d.png +0 -0
  10. package/dist/lib/jvs-ppt-ui/img/dl.png +0 -0
  11. package/dist/lib/jvs-ppt-ui/img/l.png +0 -0
  12. package/dist/lib/jvs-ppt-ui.es.js +44973 -43349
  13. package/dist/lib/jvs-ppt-ui.umd.js +102 -102
  14. package/dist/lib/style.css +1 -1
  15. package/package.json +1 -1
  16. package/scripts/check-icons.mjs +22 -0
  17. package/scripts/patch-anim-icons.mjs +45 -0
  18. package/src/assets/iconfont/iconfont.css +187 -27
  19. package/src/assets/iconfont/iconfont.js +1 -1
  20. package/src/assets/iconfont/iconfont.json +345 -65
  21. package/src/assets/iconfont/iconfont.ttf +0 -0
  22. package/src/assets/iconfont/iconfont.woff +0 -0
  23. package/src/assets/iconfont/iconfont.woff2 +0 -0
  24. package/src/components/CanvasArea.vue +218 -193
  25. package/src/components/ChartTypeSelector.vue +509 -148
  26. package/src/components/Header.vue +327 -44
  27. package/src/components/PresentationGroupChild.vue +44 -13
  28. package/src/components/PresentationMode.vue +84 -33
  29. package/src/components/QuickBorderPopover.vue +51 -11
  30. package/src/components/QuickColorPopover.vue +65 -21
  31. package/src/components/SidebarLeft.vue +286 -107
  32. package/src/components/SidebarRight.vue +45 -38
  33. package/src/components/TemplateGallery.vue +1110 -1237
  34. package/src/components/ToastNotification.vue +2 -2
  35. package/src/components/Toolbar.vue +825 -733
  36. package/src/components/canvas/CanvasQuickToolbar.vue +17 -30
  37. package/src/components/canvas/elements/CanvasGroupElement.vue +6 -0
  38. package/src/components/canvas/elements/CanvasShapeElement.vue +198 -38
  39. package/src/components/canvas/elements/CanvasTextElement.vue +4 -3
  40. package/src/components/sidebar/common/SidebarAlignGroup.vue +104 -50
  41. package/src/components/sidebar/common/SidebarBorderAccordion.vue +375 -148
  42. package/src/components/sidebar/common/SidebarPositionGroup.vue +358 -149
  43. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +429 -203
  44. package/src/components/sidebar/common/SidebarShadowAccordion.vue +844 -416
  45. package/src/components/sidebar/panels/GradientEditor.vue +671 -316
  46. package/src/components/sidebar/panels/LayoutThumbSvg.vue +33 -16
  47. package/src/components/sidebar/panels/SidebarAnimationPanel.vue +701 -138
  48. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  49. package/src/components/sidebar/panels/SidebarDesignPanel.vue +1754 -416
  50. package/src/components/sidebar/panels/SidebarMediaFormatPanel.vue +12 -0
  51. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +813 -174
  52. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  53. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1938 -1427
  54. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +791 -210
  55. package/src/composables/canvas/useCanvasShortcuts.ts +20 -6
  56. package/src/composables/canvas/useCanvasText.ts +1 -1
  57. package/src/composables/canvas/useCanvasTransform.ts +44 -0
  58. package/src/composables/presetThemes.ts +43 -44
  59. package/src/composables/sidebar/useSidebarAnimation.ts +101 -25
  60. package/src/composables/sidebar/useSidebarDesign.ts +30 -11
  61. package/src/composables/sidebar/useSidebarElementTransform.ts +10 -1
  62. package/src/composables/sidebar/useSidebarImage.ts +10 -2
  63. package/src/composables/sidebar/useSidebarShapeFormat.ts +562 -107
  64. package/src/composables/useSlides.ts +331 -153
  65. package/src/main.ts +2 -0
  66. package/src/style.css +926 -359
  67. package/src/utils/canvas/elementStyles.ts +42 -1
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
- withDefaults(defineProps<{
2
+ import { ref, computed } from 'vue'
3
+ import { useSlides } from '../../../composables/useSlides'
4
+
5
+ const props = withDefaults(defineProps<{
3
6
  elementX: number
4
7
  elementY: number
5
8
  elementW: number
@@ -32,191 +35,342 @@ const emit = defineEmits<{
32
35
  (e: 'toggleFlipV'): void
33
36
  (e: 'adjustZIndex', command: any): void
34
37
  }>()
38
+
39
+ const { activeSlide, selectedElementId, selectedElementIds } = useSlides()
40
+
41
+ const selectedIds = computed(() => {
42
+ if (selectedElementIds.value && selectedElementIds.value.length > 0) {
43
+ return selectedElementIds.value
44
+ }
45
+ if (selectedElementId.value) {
46
+ return [selectedElementId.value]
47
+ }
48
+ return []
49
+ })
50
+
51
+ const slideElements = computed(() => activeSlide.value?.elements || [])
52
+
53
+ const selectedIndices = computed(() => {
54
+ const ids = selectedIds.value
55
+ if (!ids.length) return []
56
+ return slideElements.value
57
+ .map((el: any, index: number) => (ids.includes(el.id) ? index : -1))
58
+ .filter((idx: number) => idx !== -1)
59
+ })
60
+
61
+ const canMoveUp = computed(() => {
62
+ if (selectedIndices.value.length === 0 || slideElements.value.length <= 1) return false
63
+ const total = slideElements.value.length
64
+ const allAtTop = selectedIndices.value.every((idx: number) => idx >= total - selectedIndices.value.length)
65
+ return !allAtTop
66
+ })
67
+
68
+ const canMoveDown = computed(() => {
69
+ if (selectedIndices.value.length === 0 || slideElements.value.length <= 1) return false
70
+ const allAtBottom = selectedIndices.value.every((idx: number) => idx < selectedIndices.value.length)
71
+ return !allAtBottom
72
+ })
73
+
74
+ const canMoveTop = computed(() => canMoveUp.value)
75
+ const canMoveBottom = computed(() => canMoveDown.value)
76
+
77
+ const rotateInputRef = ref<HTMLInputElement | null>(null)
78
+ const rotateDisplayLen = computed(() => Math.max(1, String(props.elementRotate ?? 0).length))
79
+
80
+ function focusRotateInput(e: MouseEvent) {
81
+ if (e.target !== rotateInputRef.value) {
82
+ rotateInputRef.value?.focus()
83
+ rotateInputRef.value?.select()
84
+ }
85
+ }
86
+
87
+ function onRotateInput(e: Event) {
88
+ const target = e.target as HTMLInputElement
89
+ const raw = target.value
90
+ if (raw === '' || raw === '-') return
91
+ const val = Number(raw)
92
+ if (isNaN(val)) return
93
+ let deg = Math.round(val) % 360
94
+ if (deg < 0) deg += 360
95
+ emit('update:elementRotate', deg)
96
+ }
97
+
98
+ function onRotateBlur(e: Event) {
99
+ const target = e.target as HTMLInputElement
100
+ if (target.value === '' || target.value === '-' || isNaN(Number(target.value))) {
101
+ target.value = String(props.elementRotate ?? 0)
102
+ } else {
103
+ let val = Math.round(Number(target.value)) % 360
104
+ if (val < 0) val += 360
105
+ target.value = String(val)
106
+ emit('update:elementRotate', val)
107
+ }
108
+ }
35
109
  </script>
36
110
 
37
111
  <template>
38
- <div class="control-group sidebar-position-group"
39
- style="padding: 0 16px; margin-top: 8px; margin-bottom: 8px; border-bottom: 1px solid #EEEFF0; padding-bottom: 12px; display: flex; flex-direction: column; gap: 8px;">
40
- <!-- X & Y -->
41
- <div v-if="!hidePosition" style="display: grid; grid-template-columns: 1fr 1fr 32px; gap: 8px;">
42
- <div class="custom-num-input" style="height: 32px;">
43
- <span class="custom-num-prefix" style="width: 20px; text-align: left;">X</span>
44
- <input type="number" step="any" :value="elementX"
45
- @input="emit('update:elementX', Number(($event.target as HTMLInputElement).value))"
46
- class="custom-num-field" />
47
- </div>
48
- <div class="custom-num-input" style="height: 32px;">
49
- <span class="custom-num-prefix" style="width: 20px; text-align: left;">Y</span>
50
- <input type="number" step="any" :value="elementY"
51
- @input="emit('update:elementY', Number(($event.target as HTMLInputElement).value))"
52
- class="custom-num-field" />
112
+ <div class="control-group sidebar-position-group">
113
+ <!-- Row 1: X & Y -->
114
+ <div v-if="!hidePosition" class="pos-row">
115
+ <div class="pos-main-cols">
116
+ <div class="custom-num-input">
117
+ <span class="custom-num-prefix">X</span>
118
+ <input type="number" step="any" :value="elementX"
119
+ @input="emit('update:elementX', Number(($event.target as HTMLInputElement).value))"
120
+ class="custom-num-field" />
121
+ </div>
122
+ <div class="custom-num-input">
123
+ <span class="custom-num-prefix">Y</span>
124
+ <input type="number" step="any" :value="elementY"
125
+ @input="emit('update:elementY', Number(($event.target as HTMLInputElement).value))"
126
+ class="custom-num-field" />
127
+ </div>
53
128
  </div>
129
+ <div class="pos-side-placeholder"></div>
54
130
  </div>
55
131
 
56
- <!-- W & H & Lock aspect ratio -->
57
- <div v-if="!hideDimensions"
58
- style="display: grid; grid-template-columns: 1fr 1fr 32px; gap: 8px; align-items: center;">
59
- <div class="custom-num-input" style="height: 32px;">
60
- <span class="custom-num-prefix" style="width: 20px; text-align: left;">W</span>
61
- <input type="number" step="any" :value="elementW"
62
- @input="emit('update:elementW', Number(($event.target as HTMLInputElement).value))"
63
- class="custom-num-field" />
64
- </div>
65
- <div class="custom-num-input" :class="{ 'is-disabled': disableHeight }" style="height: 32px;">
66
- <span class="custom-num-prefix" style="width: 20px; text-align: left;">H</span>
67
- <input type="number" step="any" :value="elementH" :disabled="disableHeight" :readonly="disableHeight"
68
- @input="!disableHeight && emit('update:elementH', Number(($event.target as HTMLInputElement).value))"
69
- class="custom-num-field" />
132
+ <!-- Row 2: W & H & Lock aspect ratio -->
133
+ <div v-if="!hideDimensions" class="pos-row">
134
+ <div class="pos-main-cols">
135
+ <div class="custom-num-input">
136
+ <span class="custom-num-prefix">W</span>
137
+ <input type="number" step="any" :value="elementW"
138
+ @input="emit('update:elementW', Number(($event.target as HTMLInputElement).value))"
139
+ class="custom-num-field" />
140
+ </div>
141
+ <div class="custom-num-input" :class="{ 'is-disabled': disableHeight }">
142
+ <span class="custom-num-prefix">H</span>
143
+ <input type="number" step="any" :value="elementH" :disabled="disableHeight" :readonly="disableHeight"
144
+ @input="!disableHeight && emit('update:elementH', Number(($event.target as HTMLInputElement).value))"
145
+ class="custom-num-field" />
146
+ </div>
70
147
  </div>
71
- <!-- aspect lock -->
72
- <div class="aspect-lock-btn" :class="{ 'is-active': isFixedRatio }" :title="isFixedRatio ? '取消锁定宽高比' : '锁定宽高比'"
148
+ <!-- Aspect lock button -->
149
+ <button type="button" class="aspect-lock-btn" :class="{ 'is-active': isFixedRatio }"
150
+ :title="isFixedRatio ? '取消锁定宽高比' : '锁定宽高比'"
73
151
  @click="emit('update:isFixedRatio', !isFixedRatio)">
74
- <svg class="icon" style="width: 16px; height: 16px;">
152
+ <svg class="icon" style="width: 16px; height: 16px; fill: currentColor;">
75
153
  <use :xlink:href="isFixedRatio ? '#icon-suoding1' : '#icon-quxiaosuoding'"></use>
76
154
  </svg>
77
- </div>
155
+ </button>
78
156
  </div>
79
157
 
80
- <!-- Rotate & Flip Horizontal & Flip Vertical -->
81
- <div v-if="showRotate || showFlips" style="display: grid; gap: 8px; grid-template-columns: 1fr 1fr 32px;">
82
- <!-- Rotate -->
83
- <div v-if="showRotate" class="custom-num-input" style="height: 32px; position: relative;">
84
- <span class="custom-num-prefix" style="display: flex; align-items: center;">
85
- <svg class="icon" style="width: 16px; height: 16px; margin-right: 6px;">
86
- <use xlink:href="#icon-jiaodu"></use>
87
- </svg>
88
- </span>
89
- <input type="number" :value="elementRotate"
90
- @input="emit('update:elementRotate', Number(($event.target as HTMLInputElement).value))"
91
- class="custom-num-field" />
92
- <span class="angle-icon" :style="{ left: `calc(30px + ${String(elementRotate ?? 0).length * 8}px)` }">°</span>
93
- </div>
94
- <div v-else></div>
95
-
96
- <!-- Flips -->
97
- <div v-if="showFlips" style="display: grid; gap: 8px; grid-template-columns: 1fr 1fr;">
98
- <div class="grid-btn" title="水平翻转" @click="emit('toggleFlipH')"
99
- :style="isFlipH ? { color: 'var(--accent-primary)', background: 'var(--bg-active)' } : { color: 'var(--text-secondary)' }"
100
- style="height: 32px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; box-sizing: border-box; border: none; background: #F5F6F7; flex-shrink: 0;">
101
- <svg class="icon" style="width: 16px; height: 16px;">
102
- <use xlink:href="#icon-shuipingfanzhuan"></use>
103
- </svg>
158
+ <!-- Row 3: Rotate & Flips -->
159
+ <div v-if="showRotate || showFlips" class="pos-row">
160
+ <div class="pos-main-cols">
161
+ <!-- Rotate in Col 1 (matches width of X and W exactly) -->
162
+ <div v-if="showRotate" class="custom-num-input rotate-input-wrapper" @click="focusRotateInput">
163
+ <span class="custom-num-prefix angle-prefix-icon">
164
+ <svg class="icon" style="width: 14px; height: 14px; fill: currentColor;">
165
+ <use xlink:href="#icon-jiaodu"></use>
166
+ </svg>
167
+ </span>
168
+ <input
169
+ ref="rotateInputRef"
170
+ type="text"
171
+ inputmode="numeric"
172
+ :value="elementRotate ?? 0"
173
+ @input="onRotateInput"
174
+ @blur="onRotateBlur"
175
+ @keydown.enter="($event.target as HTMLElement).blur()"
176
+ class="custom-num-field rotate-num-field"
177
+ :style="{ width: `calc(${rotateDisplayLen}ch + 6px)` }"
178
+ />
179
+ <span class="angle-deg-symbol">°</span>
104
180
  </div>
105
- <div class="grid-btn" title="垂直翻转" @click="emit('toggleFlipV')"
106
- :style="isFlipV ? { color: 'var(--accent-primary)', background: 'var(--bg-active)' } : { color: 'var(--text-secondary)' }"
107
- style="height: 32px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; box-sizing: border-box; border: none; background: #F5F6F7; flex-shrink: 0;">
108
- <svg class="icon" style="width: 16px; height: 16px;">
109
- <use xlink:href="#icon-chuizhifanzhuan"></use>
110
- </svg>
181
+ <div v-else class="empty-col-placeholder"></div>
182
+
183
+ <!-- Flips in Col 2 (matches boundaries of Y and H exactly) -->
184
+ <div v-if="showFlips" class="flips-pair">
185
+ <button type="button" class="pos-action-btn" title="水平翻转"
186
+ @click="emit('toggleFlipH')">
187
+ <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4">
188
+ <polygon points="8,2 2,14 8,14" fill="none" stroke="currentColor" />
189
+ <polygon points="8,2 14,14 8,14" fill="currentColor" stroke="currentColor" />
190
+ <line x1="8" y1="1" x2="8" y2="15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
191
+ </svg>
192
+ </button>
193
+ <button type="button" class="pos-action-btn" title="垂直翻转"
194
+ @click="emit('toggleFlipV')">
195
+ <svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4">
196
+ <polygon points="14,8 2,2 2,8" fill="none" stroke="currentColor" />
197
+ <polygon points="14,8 2,14 2,8" fill="currentColor" stroke="currentColor" />
198
+ <line x1="1" y1="8" x2="15" y2="8" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
199
+ </svg>
200
+ </button>
111
201
  </div>
112
202
  </div>
203
+ <div class="pos-side-placeholder"></div>
113
204
  </div>
114
- <!-- Layer row -->
115
- <div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 32px; gap: 8px;">
116
- <div class="grid-btn" title="上移一层" @click="emit('adjustZIndex', 'forward')">
117
- <svg class="icon" style="width: 16px; height: 16px;">
118
- <use xlink:href="#icon-shangyiyiceng"></use>
119
- </svg>
120
- </div>
121
- <div class="grid-btn" title="下移一层" @click="emit('adjustZIndex', 'backward')">
122
- <svg class="icon" style="width: 16px; height: 16px;">
123
- <use xlink:href="#icon-xiayiyiceng"></use>
124
- </svg>
125
- </div>
126
- <div class="grid-btn" title="置于顶层" @click="emit('adjustZIndex', 'front')">
127
- <svg class="icon" style="width: 16px; height: 16px;">
128
- <use xlink:href="#icon-zhiyudingceng"></use>
129
- </svg>
130
- </div>
131
- <div class="grid-btn" title="置于底层" @click="emit('adjustZIndex', 'back')">
132
- <svg class="icon" style="width: 16px; height: 16px;">
133
- <use xlink:href="#icon-zhiyudiceng"></use>
134
- </svg>
205
+
206
+ <!-- Row 4: Layer ordering buttons (上移一层, 下移一层, 置于顶层, 置于底层) -->
207
+ <div class="pos-row">
208
+ <div class="pos-main-cols">
209
+ <div class="layer-buttons-quad">
210
+ <!-- 上移一层 -->
211
+ <button type="button" class="pos-action-btn" :class="{ 'is-disabled': !canMoveUp }"
212
+ :disabled="!canMoveUp" title="上移一层" @click="canMoveUp && emit('adjustZIndex', 'forward')">
213
+ <svg class="icon" style="width: 16px; height: 16px;">
214
+ <use :xlink:href="canMoveUp ? '#icon-shangyiyiceng' : '#icon-shangyiyiceng1'"></use>
215
+ </svg>
216
+ </button>
217
+
218
+ <!-- 下移一层 -->
219
+ <button type="button" class="pos-action-btn" :class="{ 'is-disabled': !canMoveDown }"
220
+ :disabled="!canMoveDown" title="下移一层" @click="canMoveDown && emit('adjustZIndex', 'backward')">
221
+ <svg class="icon" style="width: 16px; height: 16px;">
222
+ <use :xlink:href="canMoveDown ? '#icon-xiayiyiceng' : '#icon-xiayiyiceng1'"></use>
223
+ </svg>
224
+ </button>
225
+
226
+ <!-- 置于顶层 -->
227
+ <button type="button" class="pos-action-btn" :class="{ 'is-disabled': !canMoveTop }"
228
+ :disabled="!canMoveTop" title="置于顶层" @click="canMoveTop && emit('adjustZIndex', 'front')">
229
+ <svg class="icon" style="width: 16px; height: 16px;">
230
+ <use :xlink:href="canMoveTop ? '#icon-zhiyudingceng' : '#icon-zhiyudingceng1'"></use>
231
+ </svg>
232
+ </button>
233
+
234
+ <!-- 置于底层 -->
235
+ <button type="button" class="pos-action-btn" :class="{ 'is-disabled': !canMoveBottom }"
236
+ :disabled="!canMoveBottom" title="置于底层" @click="canMoveBottom && emit('adjustZIndex', 'back')">
237
+ <svg class="icon" style="width: 16px; height: 16px;">
238
+ <use :xlink:href="canMoveBottom ? '#icon-zhiyudiceng' : '#icon-zhiyudiceng1'"></use>
239
+ </svg>
240
+ </button>
241
+ </div>
135
242
  </div>
243
+ <div class="pos-side-placeholder"></div>
136
244
  </div>
137
245
  <slot />
138
246
  </div>
139
247
  </template>
140
248
 
141
249
  <style scoped>
142
- .custom-num-input {
250
+ .sidebar-position-group {
251
+ padding: 0 16px;
252
+ margin-top: 4px;
253
+ margin-bottom: 12px;
254
+ border-bottom: 1px solid #edf0f3;
255
+ padding-bottom: 12px;
143
256
  display: flex;
144
- align-items: center;
145
- background: #F5F6F7;
146
- border: 1px solid #F5F6F7;
147
- border-radius: 4px;
148
- padding: 0 8px;
149
- height: 32px;
150
- box-sizing: border-box;
151
- transition: border-color 0.15s ease;
257
+ flex-direction: column;
258
+ gap: 8px;
152
259
  }
153
260
 
154
- .custom-num-input:focus-within {
155
- border-color: var(--accent-primary, #7c3aed);
156
- }
157
-
158
- .custom-num-input.is-disabled {
159
- background: #f8fafc;
160
- cursor: not-allowed;
161
- opacity: 0.85;
261
+ .pos-row {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 8px;
265
+ width: 100%;
162
266
  }
163
267
 
164
- .custom-num-input.is-disabled .custom-num-prefix {
165
- color: var(--text-muted, #94a3b8);
268
+ .pos-main-cols {
269
+ flex: 1;
270
+ display: grid;
271
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
272
+ gap: 8px;
273
+ align-items: center;
274
+ min-width: 0;
166
275
  }
167
276
 
168
- .custom-num-input.is-disabled .custom-num-field {
169
- color: var(--text-muted, #94a3b8);
170
- cursor: not-allowed;
277
+ .pos-side-placeholder {
278
+ width: 34px;
279
+ height: 34px;
280
+ flex-shrink: 0;
281
+ visibility: hidden;
171
282
  pointer-events: none;
172
- user-select: none;
173
- }
174
-
175
- .custom-num-input.is-disabled:hover,
176
- .custom-num-input.is-disabled:focus-within {
177
- border-color: #F5F6F7;
178
283
  }
179
284
 
180
285
  .aspect-lock-btn {
181
- width: 32px;
182
- height: 32px;
183
- border-radius: 6px;
286
+ width: 34px;
287
+ height: 34px;
288
+ border-radius: 8px;
184
289
  display: flex;
185
290
  align-items: center;
186
291
  justify-content: center;
187
292
  cursor: pointer;
188
- transition: all 0.2s ease;
293
+ transition: all 0.15s ease;
189
294
  box-sizing: border-box;
190
295
  flex-shrink: 0;
191
- background: #F5F6F7;
192
- color: var(--text-secondary, #64748b);
193
- border: none;
296
+ background: #f8f9fa;
297
+ color: #64748b;
298
+ border: 1px solid #edf0f3;
299
+ padding: 0;
194
300
  }
195
301
 
196
- .aspect-lock-btn:hover {
197
- background: #eef2ff;
198
- color: var(--accent-primary, #7c3aed);
302
+ /* 仅在未选中状态下应用鼠标移入浅蓝背景 */
303
+ .aspect-lock-btn:not(.is-active):hover {
304
+ background: rgba(30, 111, 255, 0.12);
305
+ color: #1E6FFF;
306
+ border-color: rgba(30, 111, 255, 0.4);
199
307
  }
200
308
 
201
- .aspect-lock-btn.is-active {
202
- background: var(--accent-primary, #7c3aed) !important;
309
+ /* 选中状态:以选中的 #1E6FFF 为主,鼠标移入时不覆盖背景色 */
310
+ .aspect-lock-btn.is-active,
311
+ .aspect-lock-btn.is-active:hover {
312
+ background: #1E6FFF !important;
203
313
  color: #ffffff !important;
314
+ border-color: #1E6FFF !important;
204
315
  }
205
316
 
206
- .aspect-lock-btn.is-active svg {
207
- fill: #ffffff !important;
317
+ .flips-pair {
318
+ display: grid;
319
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
320
+ gap: 8px;
321
+ width: 100%;
322
+ min-width: 0;
208
323
  }
209
324
 
210
- .aspect-lock-btn.is-active:hover {
211
- background: var(--accent-primary, #7c3aed) !important;
212
- opacity: 0.92;
325
+ .layer-buttons-quad {
326
+ grid-column: 1 / -1;
327
+ display: grid;
328
+ grid-template-columns: repeat(4, minmax(0, 1fr));
329
+ gap: 8px;
330
+ width: 100%;
331
+ min-width: 0;
332
+ }
333
+
334
+ .empty-col-placeholder {
335
+ height: 34px;
336
+ visibility: hidden;
337
+ }
338
+
339
+ .custom-num-input {
340
+ display: flex;
341
+ align-items: center;
342
+ background: #f8f9fa;
343
+ border: 1px solid #edf0f3;
344
+ border-radius: 8px;
345
+ padding: 0 10px;
346
+ height: 34px;
347
+ box-sizing: border-box;
348
+ transition: all 0.15s ease;
349
+ position: relative;
350
+ min-width: 0;
351
+ width: 100%;
352
+ }
353
+
354
+ .custom-num-input:hover {
355
+ border-color: #e2e8f0;
356
+ }
357
+
358
+ .custom-num-input:focus-within {
359
+ background: #ffffff;
360
+ border-color: #1E6FFF;
361
+ box-shadow: 0 0 0 2px rgba(30, 111, 255, 0.12);
213
362
  }
214
363
 
215
364
  .custom-num-prefix {
216
365
  font-size: 12px;
217
- color: var(--text-secondary, #64748b);
366
+ font-weight: 500;
367
+ color: #94a3b8;
218
368
  white-space: nowrap;
219
369
  user-select: none;
370
+ margin-right: 6px;
371
+ display: flex;
372
+ align-items: center;
373
+ flex-shrink: 0;
220
374
  }
221
375
 
222
376
  .custom-num-field {
@@ -224,9 +378,11 @@ const emit = defineEmits<{
224
378
  border: none;
225
379
  background: transparent;
226
380
  outline: none;
227
- font-size: 12px;
228
- color: var(--text-primary, #1e293b);
229
- padding: 0 4px;
381
+ font-size: 13px;
382
+ font-weight: 500;
383
+ color: #1e293b;
384
+ padding: 0;
385
+ min-width: 0;
230
386
  }
231
387
 
232
388
  .custom-num-field::-webkit-outer-spin-button,
@@ -235,32 +391,85 @@ const emit = defineEmits<{
235
391
  margin: 0;
236
392
  }
237
393
 
238
- .angle-icon {
239
- position: absolute;
240
- top: 50%;
241
- transform: translateY(calc(-50% - 2px));
242
- pointer-events: none;
394
+ .rotate-input-wrapper {
395
+ cursor: text;
396
+ }
397
+
398
+ .rotate-num-field {
399
+ width: auto;
400
+ min-width: 1ch;
401
+ flex: none;
402
+ font-variant-numeric: tabular-nums;
403
+ box-sizing: content-box;
404
+ }
405
+
406
+ .angle-prefix-icon {
407
+ color: #64748b;
408
+ margin-right: 6px;
409
+ flex-shrink: 0;
410
+ }
411
+
412
+ .angle-deg-symbol {
243
413
  font-size: 12px;
244
- color: var(--text-secondary, #64748b);
414
+ font-weight: 500;
415
+ color: #64748b;
245
416
  margin-left: 2px;
417
+ line-height: 1;
418
+ user-select: none;
419
+ pointer-events: none;
420
+ flex-shrink: 0;
246
421
  }
247
422
 
248
- .grid-btn {
249
- height: 32px;
250
- border-radius: 4px;
423
+ .pos-action-btn {
424
+ height: 34px;
425
+ border-radius: 8px;
251
426
  display: flex;
252
427
  align-items: center;
253
428
  justify-content: center;
254
429
  cursor: pointer;
255
- background: #F5F6F7;
256
- color: var(--text-primary, #1e293b);
257
- border: none;
430
+ background: #f8f9fa;
431
+ color: #64748b;
432
+ border: 1px solid #edf0f3;
258
433
  box-sizing: border-box;
259
434
  transition: all 0.15s ease;
435
+ padding: 0;
436
+ width: 100%;
437
+ min-width: 0;
438
+ }
439
+
440
+ /* 仅在未选中且未禁用状态下应用鼠标移入浅蓝背景 */
441
+ .pos-action-btn:not(.is-active):not(:disabled):not(.is-disabled):hover {
442
+ background: rgba(30, 111, 255, 0.12);
443
+ color: #1E6FFF;
444
+ border-color: rgba(30, 111, 255, 0.4);
260
445
  }
261
446
 
262
- .grid-btn:hover {
263
- background: #eef2ff;
264
- color: var(--accent-primary, #7c3aed);
447
+ /* 禁用状态 */
448
+ .pos-action-btn:disabled,
449
+ .pos-action-btn.is-disabled {
450
+ cursor: not-allowed;
451
+ opacity: 0.85;
452
+ background: #f8f9fa;
453
+ border-color: #edf0f3;
454
+ }
455
+
456
+ /* 选中状态:以选中的 #1E6FFF 为主,鼠标移入时不覆盖背景色 */
457
+ .pos-action-btn.is-active,
458
+ .pos-action-btn.is-active:hover {
459
+ background: #1E6FFF !important;
460
+ color: #ffffff !important;
461
+ border-color: #1E6FFF !important;
462
+ }
463
+
464
+ .custom-num-input.is-disabled {
465
+ background: #f8fafc;
466
+ cursor: not-allowed;
467
+ opacity: 0.7;
468
+ }
469
+
470
+ .custom-num-input.is-disabled .custom-num-prefix,
471
+ .custom-num-input.is-disabled .custom-num-field {
472
+ color: #94a3b8;
473
+ cursor: not-allowed;
265
474
  }
266
475
  </style>