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 +114 -0
- package/package.json +1 -1
- package/src/IconPicker.vue +19 -13
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
package/src/IconPicker.vue
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
140
|
-
|
|
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 ? '-
|
|
189
|
-
list = list.map(n => (/-line$|-
|
|
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
|
+
);
|