cd-icon-picker 0.1.0 → 0.1.1

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/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # cd-icon-picker
2
+
3
+ Vue 3 图标选择器,支持 TDesign、RemixIcon 与自定义 SVG 精灵。默认中文交互,带搜索、分类与分页。
4
+
5
+ ## 特性
6
+
7
+ - 支持来源切换:`Remix`、`TDesign`、`自定义`
8
+ - 支持风格切换:`描边` 与 `填充`
9
+ - 搜索支持字母子序列匹配(忽略大小写与连接符)
10
+ - 自定义 SVG 通过精灵引用(`<symbol>`),无需单独组件注册
11
+ - 输入展示支持两种模式:`mode='text'` 显示文字+图标,`mode='icon'` 仅图标
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm i cd-icon-picker tdesign-vue-next remixicon
17
+ ```
18
+
19
+ 在应用入口注册:
20
+
21
+ ```ts
22
+ import { createApp } from 'vue';
23
+ import TDesign from 'tdesign-vue-next';
24
+ import 'tdesign-vue-next/es/style/index.css';
25
+ import 'remixicon/fonts/remixicon.css';
26
+
27
+ import IconPickerPlugin from 'cd-icon-picker';
28
+
29
+ const app = createApp(App);
30
+ app.use(TDesign);
31
+ app.use(IconPickerPlugin);
32
+ app.mount('#app');
33
+ ```
34
+
35
+ ## 快速使用
36
+
37
+ ```vue
38
+ <CdIconPicker
39
+ v-model:type="iconType"
40
+ v-model:value="iconValue"
41
+ mode="text"
42
+ width="200px"
43
+ @change="(type, value) => { /*...*/ }"
44
+ />
45
+ ```
46
+
47
+ - `mode='text'`:输入框显示图标名称与前缀图标
48
+ - `mode='icon'`:仅显示图标,点击弹出选择对话框
49
+
50
+ ## 组件属性(IconPicker)
51
+
52
+ - `type: 'remix' | 'tdesign' | 'custom'` 当前来源(`v-model:type`)
53
+ - `value: string` 当前图标基础名(`v-model:value`)
54
+ - `tdesign?: string` 旧版双向绑定(可选)
55
+ - `remixicon?: string` 旧版双向绑定(可选)
56
+ - `svg?: string` 旧版双向绑定(可选)
57
+ - `pageSize: number = 140` 每页显示数量
58
+ - `copy: boolean = false` 选择后是否复制图标名到剪贴板
59
+ - `diyIcon: string[] | { prefix: string; icons: string[]; categoryMap?: Record<string, string[]> }` 自定义 SVG 配置
60
+ - `placeholder: string = '选择图标'` 输入框占位
61
+ - `searchPlaceholder: string = '搜索图标'` 搜索框占位
62
+ - `readonly: boolean = true` 输入框只读
63
+ - `categoryMap?: Record<string, string[]>` 类别映射(可覆写内置)
64
+ - `mode: 'icon' | 'text' = 'text'` 展示模式
65
+
66
+ 事件:
67
+ - `update:type`、`update:value`、`change(type, value)`
68
+
69
+ ## 自定义 SVG
70
+
71
+ 推荐使用 `vite-plugin-svg-icons` 自动注册 `src/svg-icon` 目录下的 SVG 文件为精灵:
72
+
73
+ ```ts
74
+ // vite.config.ts
75
+ import { defineConfig } from 'vite';
76
+ import vue from '@vitejs/plugin-vue';
77
+ import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
78
+ import path from 'path';
79
+
80
+ export default defineConfig({
81
+ plugins: [
82
+ vue(),
83
+ createSvgIconsPlugin({
84
+ iconDirs: [path.resolve(process.cwd(), 'src/svg-icon')],
85
+ symbolId: 'icon-[name]',
86
+ }),
87
+ ],
88
+ });
89
+ ```
90
+
91
+ 在入口注册精灵:
92
+
93
+ ```ts
94
+ // main.ts
95
+ import 'virtual:svg-icons-register';
96
+ ```
97
+
98
+ 构造 `diyIcon`:
99
+
100
+ ```ts
101
+ // 自动读取文件名为 icons 列表
102
+ const files = import.meta.glob('./svg-icon/*.svg');
103
+ const icons = Object.keys(files).map(p => p.split('/').pop()!.replace('.svg',''));
104
+ const diyIcon = { prefix: 'icon', icons };
105
+ ```
106
+
107
+ 随后传入组件的 `:diy-icon="diyIcon"`,即可在“自定义”来源下选择上述 SVG。
108
+
109
+ ## 搜索与分类
110
+
111
+ - 搜索采用字母子序列匹配:输入 `aru` 可匹配 `ri-arrow-up-line/filled`
112
+ - 当 `type='remix'` 且 `value` 不含后缀时,会按当前风格自动补全
113
+ - 描边:`-line`
114
+ - 填充:`-filled`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cd-icon-picker",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Vue 3 icon picker supporting TDesign & RemixIcon, with custom SVG via :diy-icon.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -91,6 +91,7 @@ const props = defineProps({
91
91
  readonly: { type: Boolean, default: true },
92
92
  categoryMap: { type: Object as PropType<Record<string, string[]>> },
93
93
  mode: { type: String as PropType<'icon' | 'text'>, default: 'text' },
94
+ type: { type: String as PropType<'remix' | 'tdesign' | 'custom'>, default: 'remix' },
94
95
  });
95
96
 
96
97
  const emit = defineEmits(['change', 'update:value', 'update:tdesign', 'update:remixicon', 'update:svg']);
@@ -112,7 +113,7 @@ const rawIcons = computed<string[]>(() => {
112
113
  });
113
114
 
114
115
  const styleType = ref<'outlined' | 'filled'>('outlined');
115
- const sourceType = ref< 'remix'| 'tdesign' | 'custom'>('remix');
116
+ const sourceType = ref<'remix' | 'tdesign' | 'custom'>('remix');
116
117
  const categoryType = ref<string>('all');
117
118
 
118
119
  const customIcons = computed(() => {
@@ -136,22 +137,18 @@ const displayValue = computed({
136
137
  });
137
138
 
138
139
  watchEffect(() => {
139
- const bySource = sourceType.value === 'tdesign' ? props.tdesign : sourceType.value === 'remix' ? props.remixicon : props.svg;
140
- currentSelect.value = props.value || bySource || '';
140
+ sourceType.value = (props.type as any) || sourceType.value;
141
+ let v = props.value || '';
142
+ if (sourceType.value === 'remix' && v && !/-line$|-filled$/.test(v)) {
143
+ v = v + (styleType.value === 'filled' ? '-filled' : '-line');
144
+ }
145
+ currentSelect.value = v;
141
146
  });
142
147
 
143
148
  watch([rawIcons, styleType, sourceType], () => {
144
149
  page.value = 1;
145
150
  });
146
151
 
147
- watch(
148
- () => currentSelect.value,
149
- v => {
150
- emit('update:value', v);
151
- emit('change', v);
152
- },
153
- );
154
-
155
152
  const searchText = ref('');
156
153
  const categoryMap = computed(() => {
157
154
  if (props.categoryMap) return props.categoryMap as Record<string, string[]>;
@@ -185,8 +182,8 @@ const filteredIcons = computed(() => {
185
182
  }
186
183
 
187
184
  if (sourceType.value === 'remix') {
188
- const suffix = useFill ? '-fill' : '-line';
189
- list = list.map(n => (/-line$|-fill$/.test(n) ? n : `${n}${suffix}`));
185
+ const suffix = useFill ? '-filled' : '-line';
186
+ list = list.map(n => (/-line$|-filled$/.test(n) ? n : `${n}${suffix}`));
190
187
  }
191
188
 
192
189
  return searchText.value ? list.filter(i => i.includes(searchText.value)) : list;
@@ -213,6 +210,9 @@ function handleClick(icon: string) {
213
210
  if (sourceType.value === 'tdesign') emit('update:tdesign', icon);
214
211
  else if (sourceType.value === 'remix') emit('update:remixicon', icon);
215
212
  else emit('update:svg', icon);
213
+
214
+ emit('update:value', icon);
215
+ emit('change', sourceType.value as any, icon as any);
216
216
  }
217
217
 
218
218
  let timer: any = null;
@@ -271,3 +271,9 @@ function handleCancel() {
271
271
  .cd-picker-pagination { display: flex; justify-content: center; padding: 8px; }
272
272
  .cd-picker-empty { padding: 24px; }
273
273
  </style>
274
+ watch(
275
+ () => sourceType.value,
276
+ v => {
277
+ emit('update:type', v);
278
+ },
279
+ );