@zzalai/leafer-point-annotation 1.0.0

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.
@@ -0,0 +1,295 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <!-- 遮罩层 -->
4
+ <div class="brush-panel-overlay" v-if="visible" @click="$emit('close')"></div>
5
+
6
+ <!-- 配置面板 -->
7
+ <div
8
+ class="brush-style-panel"
9
+ v-if="visible"
10
+ :style="panelStyle"
11
+ @click.stop
12
+ >
13
+ <div class="panel-header">
14
+ <span>笔刷配置</span>
15
+ <button class="close-btn" @click="$emit('close')">×</button>
16
+ </div>
17
+ <div class="panel-content">
18
+ <!-- 颜色选择 -->
19
+ <div class="config-item">
20
+ <label class="config-label">颜色: </label>
21
+ <div class="color-picker-wrapper">
22
+ <PickColors
23
+ v-model:value="localColor"
24
+ :predefine-colors="predefinedColors"
25
+ placement="right"
26
+ :z-index="2001"
27
+ :popupContainer="false"
28
+ />
29
+ </div>
30
+ </div>
31
+
32
+ <!-- 透明度 -->
33
+ <div class="config-item">
34
+ <label class="config-label">透明度: </label>
35
+ <input
36
+ type="range"
37
+ class="config-slider"
38
+ v-model.number="localOpacity"
39
+ min="0.1"
40
+ max="1"
41
+ step="0.01"
42
+ />
43
+ <span class="config-value">{{ Math.round(localOpacity * 100) }}%</span>
44
+ </div>
45
+
46
+ <!-- 大小 -->
47
+ <div class="config-item">
48
+ <label class="config-label">大小:</label>
49
+ <input
50
+ type="range"
51
+ class="config-slider"
52
+ v-model.number="localSize"
53
+ :min="brushStyle.minSize"
54
+ :max="brushStyle.maxSize"
55
+ />
56
+ <span class="config-value">{{ localSize }}</span>
57
+ </div>
58
+
59
+ <!-- 连续性 -->
60
+ <div class="config-item">
61
+ <label class="config-label">连续性: </label>
62
+ <input
63
+ type="range"
64
+ class="config-slider"
65
+ v-model.number="localContinuity"
66
+ min="5"
67
+ max="50"
68
+ />
69
+ <span class="config-value">{{ localContinuity }}</span>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </Teleport>
74
+ </template>
75
+
76
+ <script setup lang="ts">
77
+ import { ref, watch, computed } from 'vue';
78
+ import PickColors from 'vue-pick-colors';
79
+ import type { BrushStyle } from '@/types';
80
+
81
+ const props = defineProps<{
82
+ visible: boolean;
83
+ brushStyle: BrushStyle;
84
+ buttonRect?: DOMRect | null;
85
+ }>();
86
+
87
+ const emit = defineEmits(['close', 'update']);
88
+
89
+ // 预定义颜色
90
+ const predefinedColors = [
91
+ '#FF4D4F', '#FF7875', '#FFA940', '#FFC53D',
92
+ '#A0D911', '#52C41A', '#34D399', '#10B981',
93
+ '#06B6D4', '#0EA5E9', '#3B82F6', '#6366F1',
94
+ '#8B5CF6', '#A855F7', '#D946EF', '#EC4899',
95
+ '#000000', '#666666', '#999999', '#CCCCCC',
96
+ ];
97
+
98
+ // 本地状态
99
+ const localColor = ref(props.brushStyle.color);
100
+ const localOpacity = ref(props.brushStyle.opacity);
101
+ const localSize = ref(props.brushStyle.size);
102
+ const localContinuity = ref(props.brushStyle.continuity);
103
+
104
+ // 监听props变化(包括组件重新创建时立即执行)
105
+ watch(() => props.brushStyle, (newVal) => {
106
+ localColor.value = newVal.color;
107
+ localOpacity.value = newVal.opacity;
108
+ localSize.value = newVal.size;
109
+ localContinuity.value = newVal.continuity;
110
+ }, { deep: true, immediate: true });
111
+
112
+ // 监听本地状态变化,通知父组件
113
+ const notifyUpdate = () => {
114
+ // 确保颜色格式正确(移除可能的alpha值,因为我们有单独的opacity控制)
115
+ let color = localColor.value;
116
+
117
+ if (color.startsWith('rgba')) {
118
+ // 提取rgb部分并转换为hex
119
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
120
+ if (match) {
121
+ color = '#' +
122
+ parseInt(match[1]).toString(16).padStart(2, '0') +
123
+ parseInt(match[2]).toString(16).padStart(2, '0') +
124
+ parseInt(match[3]).toString(16).padStart(2, '0');
125
+ }
126
+ } else if (color.startsWith('rgb')) {
127
+ // 提取rgb部分并转换为hex
128
+ const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
129
+ if (match) {
130
+ color = '#' +
131
+ parseInt(match[1]).toString(16).padStart(2, '0') +
132
+ parseInt(match[2]).toString(16).padStart(2, '0') +
133
+ parseInt(match[3]).toString(16).padStart(2, '0');
134
+ }
135
+ }
136
+
137
+ emit('update', {
138
+ color: color,
139
+ opacity: localOpacity.value,
140
+ size: localSize.value,
141
+ continuity: localContinuity.value,
142
+ });
143
+ };
144
+
145
+ watch([localColor, localOpacity, localSize, localContinuity], () => {
146
+ notifyUpdate();
147
+ });
148
+
149
+ // 面板位置计算
150
+ const panelStyle = computed(() => {
151
+ if (!props.buttonRect) {
152
+ return {
153
+ left: '50%',
154
+ top: '50%',
155
+ transform: 'translate(-50%, -50%)',
156
+ };
157
+ }
158
+ const buttonCenterX = props.buttonRect.left + props.buttonRect.width / 2;
159
+ const buttonTop = props.buttonRect.top;
160
+ return {
161
+ left: `${buttonCenterX}px`,
162
+ bottom: `calc(100vh - ${buttonTop - 10}px)`,
163
+ transform: 'translateX(-50%)',
164
+ };
165
+ });
166
+ </script>
167
+
168
+ <style scoped>
169
+ .brush-panel-overlay {
170
+ position: fixed;
171
+ top: 0;
172
+ left: 0;
173
+ right: 0;
174
+ bottom: 0;
175
+ background: rgba(0, 0, 0, 0.3);
176
+ z-index: 1500;
177
+ }
178
+
179
+ .brush-style-panel {
180
+ position: fixed;
181
+ z-index: 1501;
182
+ background: white;
183
+ border-radius: 10px;
184
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
185
+ min-width: 240px;
186
+ overflow: visible;
187
+ }
188
+
189
+ .panel-header {
190
+ display: flex;
191
+ justify-content: space-between;
192
+ align-items: center;
193
+ padding: 10px 16px;
194
+ background: var(--leafer-point-color-background-light);
195
+ border-bottom: 1px solid var(--leafer-point-color-border);
196
+ border-radius: 10px 10px 0 0;
197
+ }
198
+
199
+ .panel-header span {
200
+ font-size: 14px;
201
+ font-weight: 600;
202
+ color: var(--leafer-point-color-text);
203
+ }
204
+
205
+ .close-btn {
206
+ width: 24px;
207
+ height: 24px;
208
+ border: none;
209
+ background: transparent;
210
+ border-radius: 50%;
211
+ cursor: pointer;
212
+ font-size: 18px;
213
+ color: var(--leafer-point-color-text);
214
+ display: flex;
215
+ justify-content: center;
216
+ align-items: center;
217
+ transition: all 0.2s ease;
218
+ }
219
+
220
+ .close-btn:hover {
221
+ background: var(--leafer-point-color-border);
222
+ }
223
+
224
+ .panel-content {
225
+ padding: 16px;
226
+ padding-bottom: 24px;
227
+ }
228
+
229
+ .config-item {
230
+ margin-bottom: 20px;
231
+ display: flex;
232
+ align-items: center;
233
+ }
234
+
235
+ .config-item:last-child {
236
+ margin-bottom: 0;
237
+ }
238
+
239
+ .config-label {
240
+ display: block;
241
+ font-size: 12px;
242
+ color: var(--leafer-point-color-text);
243
+ /* margin-bottom: 8px; */
244
+ min-width: 50px;
245
+ padding-right: 5px;
246
+ text-align: right;
247
+ }
248
+
249
+ .config-value {
250
+ padding-left: 5px;
251
+ font-size: 12px;
252
+ color: var(--leafer-point-color-text);
253
+ width: 30px;
254
+ }
255
+
256
+ .color-picker-wrapper {
257
+ width: 100%;
258
+ margin: -10px 0;
259
+ }
260
+
261
+ .config-slider {
262
+ /* flex: 1;
263
+ width: 100%; */
264
+ width: 200px;
265
+ height: 6px;
266
+ background: #e0e0e0;
267
+ border-radius: 3px;
268
+ outline: none;
269
+ -webkit-appearance: none;
270
+ appearance: none;
271
+ cursor: pointer;
272
+ }
273
+
274
+ .config-slider::-webkit-slider-thumb {
275
+ -webkit-appearance: none;
276
+ appearance: none;
277
+ width: 16px;
278
+ height: 16px;
279
+ background: var(--leafer-point-color-primary);
280
+ border-radius: 50%;
281
+ cursor: pointer;
282
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
283
+ border: 2px solid white;
284
+ }
285
+
286
+ .config-slider::-moz-range-thumb {
287
+ width: 16px;
288
+ height: 16px;
289
+ background: var(--leafer-point-color-primary);
290
+ border-radius: 50%;
291
+ cursor: pointer;
292
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
293
+ border: 2px solid white;
294
+ }
295
+ </style>