cd-icon-picker 1.2.2 → 1.2.4
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/package.json +3 -2
- package/src/IconPicker.vue +8 -39
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cd-icon-picker",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Vue 3 icon picker supporting TDesign & RemixIcon, with custom SVG via :diy-icon.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./index.ts"
|
|
9
|
+
".": "./index.ts",
|
|
10
|
+
"./src/Icon.vue": "./src/Icon.vue"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"src",
|
package/src/IconPicker.vue
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
<Icon v-else :icon="currentSelect || defaultIcon" class="cursor-pointer px-2 py-1" />
|
|
17
17
|
</template>
|
|
18
18
|
</t-input>
|
|
19
|
-
|
|
20
19
|
<teleport to="body">
|
|
21
20
|
<t-dialog v-model:visible="visible" width="960px" :close-on-esc-keydown="true" :close-on-overlay-click="true" @confirm="handleConfirm" @cancel="handleCancel">
|
|
22
21
|
<template #header>
|
|
@@ -62,7 +61,7 @@
|
|
|
62
61
|
<div class="cd-picker-search">
|
|
63
62
|
<t-input :placeholder="searchPlaceholder" @input="debounceHandleSearchChange" clearable />
|
|
64
63
|
</div>
|
|
65
|
-
<div v-if="sourceType === 'custom'" class="cd-picker-custom-url"
|
|
64
|
+
<div v-if="sourceType === 'custom'" class="cd-picker-custom-url">
|
|
66
65
|
<t-input v-model="customImageUrl" placeholder="输入图片URL (https://...)" clearable>
|
|
67
66
|
<template #suffix>
|
|
68
67
|
<t-button size="small" @click="handleAddCustomImage">添加</t-button>
|
|
@@ -101,7 +100,6 @@ import { ref, watchEffect, watch, computed } from 'vue';
|
|
|
101
100
|
import Icon from './Icon.vue';
|
|
102
101
|
import SvgIcon from './SvgIcon.vue';
|
|
103
102
|
import categories from '../data/categories';
|
|
104
|
-
|
|
105
103
|
const props = defineProps({
|
|
106
104
|
value: { type: String, default: '' },
|
|
107
105
|
tdesign: { type: String, default: '' },
|
|
@@ -119,18 +117,14 @@ const props = defineProps({
|
|
|
119
117
|
type: { type: String as PropType<'remix' | 'tdesign' | 'custom' | 'company'>, default: 'remix' },
|
|
120
118
|
color: { type: String, default: '' },
|
|
121
119
|
});
|
|
122
|
-
|
|
123
120
|
const emit = defineEmits(['change', 'update:value', 'update:tdesign', 'update:remixicon', 'update:svg', 'update:type', 'update:color']);
|
|
124
|
-
|
|
125
121
|
// dialog 不需要 attach/placement
|
|
126
|
-
|
|
127
122
|
const isImageUrl = (icon: string) => /^https?:\/\/.+\.(png|jpg|jpeg|gif|svg|webp)/i.test(icon) || /^[./].*\.(png|jpg|jpeg|gif|svg|webp)$/i.test(icon);
|
|
128
123
|
const isSvgMode = computed(() => sourceType.value === 'custom');
|
|
129
124
|
const svgPrefix = computed(() => {
|
|
130
125
|
const d = props.diyIcon as any;
|
|
131
126
|
return Array.isArray(d) ? 'icon' : (d?.prefix ?? 'icon');
|
|
132
127
|
});
|
|
133
|
-
|
|
134
128
|
const rawIcons = computed<string[]>(() => {
|
|
135
129
|
if (sourceType.value === 'custom') {
|
|
136
130
|
const d = props.diyIcon as any;
|
|
@@ -141,17 +135,14 @@ const rawIcons = computed<string[]>(() => {
|
|
|
141
135
|
}
|
|
142
136
|
return [] as string[];
|
|
143
137
|
});
|
|
144
|
-
|
|
145
138
|
const styleType = ref<'outlined' | 'filled'>('outlined');
|
|
146
139
|
const sourceType = ref<'remix' | 'tdesign' | 'custom' | 'company'>('remix');
|
|
147
140
|
const categoryType = ref<string>('all');
|
|
148
|
-
|
|
149
141
|
const customIcons = computed(() => {
|
|
150
142
|
const d = props.diyIcon as any;
|
|
151
143
|
const baseIcons = Array.isArray(d) ? (d as string[]) : ((d?.icons ?? []) as string[]);
|
|
152
144
|
return [...baseIcons, ...customImageList.value];
|
|
153
145
|
});
|
|
154
|
-
|
|
155
146
|
const currentSelect = ref('');
|
|
156
147
|
const visible = ref(false);
|
|
157
148
|
const page = ref(1);
|
|
@@ -161,7 +152,6 @@ const colorPickerVisible = ref(false);
|
|
|
161
152
|
const presetColors = ['#000000', '#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#34495e'];
|
|
162
153
|
const customImageUrl = ref('');
|
|
163
154
|
const customImageList = ref<string[]>([]);
|
|
164
|
-
|
|
165
155
|
const displayValue = computed({
|
|
166
156
|
get() {
|
|
167
157
|
return props.mode === 'text' ? currentSelect.value : '';
|
|
@@ -170,7 +160,6 @@ const displayValue = computed({
|
|
|
170
160
|
if (props.mode === 'text') currentSelect.value = v || '';
|
|
171
161
|
},
|
|
172
162
|
});
|
|
173
|
-
|
|
174
163
|
watch(
|
|
175
164
|
() => props.type,
|
|
176
165
|
v => {
|
|
@@ -178,7 +167,13 @@ watch(
|
|
|
178
167
|
},
|
|
179
168
|
{ immediate: true },
|
|
180
169
|
);
|
|
181
|
-
|
|
170
|
+
watch(
|
|
171
|
+
() => props.color,
|
|
172
|
+
v => {
|
|
173
|
+
color.value = v || '';
|
|
174
|
+
},
|
|
175
|
+
{ immediate: true },
|
|
176
|
+
);
|
|
182
177
|
watchEffect(() => {
|
|
183
178
|
let v = props.value || '';
|
|
184
179
|
if (sourceType.value === 'remix' && v && !/-line$|-fill$/.test(v)) {
|
|
@@ -186,11 +181,9 @@ watchEffect(() => {
|
|
|
186
181
|
}
|
|
187
182
|
currentSelect.value = v;
|
|
188
183
|
});
|
|
189
|
-
|
|
190
184
|
watch([rawIcons, styleType, sourceType], () => {
|
|
191
185
|
page.value = 1;
|
|
192
186
|
});
|
|
193
|
-
|
|
194
187
|
const searchText = ref('');
|
|
195
188
|
const categoryMap = computed(() => {
|
|
196
189
|
if (props.categoryMap) return props.categoryMap as Record<string, string[]>;
|
|
@@ -199,16 +192,12 @@ const categoryMap = computed(() => {
|
|
|
199
192
|
const map = (categories as any)[sourceType.value] || (categories as any).tdesign;
|
|
200
193
|
return map as Record<string, string[]>;
|
|
201
194
|
});
|
|
202
|
-
|
|
203
195
|
const categoryOptions = computed(() => Object.keys(categoryMap.value).filter(k => !k.endsWith('fill')));
|
|
204
|
-
|
|
205
|
-
|
|
206
196
|
const filteredIcons = computed(() => {
|
|
207
197
|
if (sourceType.value === 'custom') {
|
|
208
198
|
const list = customIcons.value;
|
|
209
199
|
return searchText.value ? list.filter(i => i.includes(searchText.value)) : list;
|
|
210
200
|
}
|
|
211
|
-
|
|
212
201
|
if (sourceType.value === 'company') {
|
|
213
202
|
// 企业图标从 categories.ts 中的 company 数据加载
|
|
214
203
|
const map: any = categoryMap.value;
|
|
@@ -224,11 +213,9 @@ const filteredIcons = computed(() => {
|
|
|
224
213
|
}
|
|
225
214
|
return searchText.value ? list.filter(i => i.includes(searchText.value)) : list;
|
|
226
215
|
}
|
|
227
|
-
|
|
228
216
|
const useFill = styleType.value === 'filled';
|
|
229
217
|
const map: any = categoryMap.value;
|
|
230
218
|
const cats = categoryOptions.value;
|
|
231
|
-
|
|
232
219
|
let list: string[] = [];
|
|
233
220
|
if (categoryType.value === 'all') {
|
|
234
221
|
for (const cat of cats) {
|
|
@@ -238,51 +225,41 @@ const filteredIcons = computed(() => {
|
|
|
238
225
|
} else {
|
|
239
226
|
list = ((useFill ? map[`${categoryType.value}fill`] : map[categoryType.value]) || map[categoryType.value] || []) as string[];
|
|
240
227
|
}
|
|
241
|
-
|
|
242
228
|
if (sourceType.value === 'remix') {
|
|
243
229
|
const suffix = useFill ? '-fill' : '-line';
|
|
244
230
|
list = list.map(n => (/-line$|-fill$/.test(n) ? n : `${n}${suffix}`));
|
|
245
231
|
}
|
|
246
|
-
|
|
247
232
|
return searchText.value ? list.filter(i => i.includes(searchText.value)) : list;
|
|
248
233
|
});
|
|
249
|
-
|
|
250
234
|
const getTotal = computed(() => filteredIcons.value.length);
|
|
251
235
|
const getPaginationList = computed(() => {
|
|
252
236
|
const start = (page.value - 1) * pageSize.value;
|
|
253
237
|
return filteredIcons.value.slice(start, start + pageSize.value);
|
|
254
238
|
});
|
|
255
|
-
|
|
256
239
|
function handlePageChange(p: number | { current: number }) {
|
|
257
240
|
const next = typeof p === 'number' ? p : (p as any).current;
|
|
258
241
|
page.value = next || 1;
|
|
259
242
|
}
|
|
260
|
-
|
|
261
243
|
const defaultIcon = 'ri-apps-line';
|
|
262
|
-
|
|
263
244
|
function handleClick(icon: string) {
|
|
264
245
|
currentSelect.value = icon;
|
|
265
246
|
if (props.copy && navigator?.clipboard) {
|
|
266
247
|
navigator.clipboard.writeText(icon).catch(() => {});
|
|
267
248
|
}
|
|
268
249
|
}
|
|
269
|
-
|
|
270
250
|
let timer: any = null;
|
|
271
251
|
function debounceHandleSearchChange(e: any) {
|
|
272
252
|
const value = typeof e === 'string' ? e : (e?.target?.value || e?.detail?.value || '');
|
|
273
253
|
if (timer) clearTimeout(timer);
|
|
274
254
|
timer = setTimeout(() => handleSearch(value), 100);
|
|
275
255
|
}
|
|
276
|
-
|
|
277
256
|
function handleSearch(value: string) {
|
|
278
257
|
searchText.value = value || '';
|
|
279
258
|
page.value = 1;
|
|
280
259
|
}
|
|
281
|
-
|
|
282
260
|
function handleOpen() {
|
|
283
261
|
visible.value = true;
|
|
284
262
|
}
|
|
285
|
-
|
|
286
263
|
function handleConfirm() {
|
|
287
264
|
visible.value = false;
|
|
288
265
|
const icon = currentSelect.value;
|
|
@@ -290,12 +267,10 @@ function handleConfirm() {
|
|
|
290
267
|
if (sourceType.value === 'tdesign') emit('update:tdesign', icon);
|
|
291
268
|
else if (sourceType.value === 'remix') emit('update:remixicon', icon);
|
|
292
269
|
else emit('update:svg', icon);
|
|
293
|
-
|
|
294
270
|
emit('update:value', icon);
|
|
295
271
|
emit('update:color', color.value);
|
|
296
272
|
emit('change', sourceType.value as any, icon as any, color.value);
|
|
297
273
|
}
|
|
298
|
-
|
|
299
274
|
function handleCancel() {
|
|
300
275
|
visible.value = false;
|
|
301
276
|
let v = props.value || '';
|
|
@@ -304,7 +279,6 @@ function handleCancel() {
|
|
|
304
279
|
}
|
|
305
280
|
currentSelect.value = v;
|
|
306
281
|
}
|
|
307
|
-
|
|
308
282
|
function handleAddCustomImage() {
|
|
309
283
|
const url = customImageUrl.value.trim();
|
|
310
284
|
if (!url) return;
|
|
@@ -318,18 +292,15 @@ function handleAddCustomImage() {
|
|
|
318
292
|
currentSelect.value = url;
|
|
319
293
|
customImageUrl.value = '';
|
|
320
294
|
}
|
|
321
|
-
|
|
322
295
|
watch(
|
|
323
296
|
() => sourceType.value,
|
|
324
297
|
v => {
|
|
325
298
|
emit('update:type', v);
|
|
326
299
|
},
|
|
327
300
|
);
|
|
328
|
-
|
|
329
301
|
</script>
|
|
330
302
|
<style>
|
|
331
303
|
.cd-icon-picker-popover .scrollbar { height: 220px; overflow: auto; }
|
|
332
|
-
|
|
333
304
|
.cd-picker-body { display: flex; gap: 12px; }
|
|
334
305
|
.cd-picker-sidebar { width: 240px; border-right: 1px solid #eee; padding-right: 12px; }
|
|
335
306
|
.cd-picker-section { margin-bottom: 12px; }
|
|
@@ -337,10 +308,8 @@ watch(
|
|
|
337
308
|
.cd-picker-cats { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: row; gap: 4px; }
|
|
338
309
|
.cd-picker-cat { padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
339
310
|
.cd-picker-cat.active { background: #f2f3f5; color: #0052d9; }
|
|
340
|
-
|
|
341
311
|
.cd-icon-only { display: flex; align-items: center; justify-content: center; border: 1px solid #e5e5e5; border-radius: 6px; padding: 6px; cursor: pointer; }
|
|
342
312
|
.cd-icon-only:hover { border-color: var(--td-brand-color, #0052d9); }
|
|
343
|
-
|
|
344
313
|
.cd-picker-main { width: 620px; }
|
|
345
314
|
.cd-picker-search { margin-bottom: 8px; }
|
|
346
315
|
.cd-picker-content { border-top: 1px solid #eee; }
|