cd-icon-picker 1.2.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/IconPicker.vue +8 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cd-icon-picker",
3
- "version": "1.2.3",
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",
@@ -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" style="padding: 12px; border-bottom: 1px solid #eee;">
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; }