index.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <template>
  2. <div class="icon-selector w100 h100">
  3. <el-input
  4. v-model="state.fontIconSearch"
  5. :placeholder="state.fontIconPlaceholder"
  6. :clearable="clearable"
  7. :disabled="disabled"
  8. :size="size"
  9. ref="inputWidthRef"
  10. @clear="onClearFontIcon"
  11. @focus="onIconFocus"
  12. @blur="onIconBlur"
  13. >
  14. <template #prepend>
  15. <SvgIcon
  16. :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
  17. class="font14"
  18. v-if="state.fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : state.fontIconPrefix?.indexOf('ele-') > -1"
  19. />
  20. <i v-else :class="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14"></i>
  21. </template>
  22. </el-input>
  23. <el-popover
  24. placement="bottom"
  25. :width="state.fontIconWidth"
  26. transition="el-zoom-in-top"
  27. popper-class="icon-selector-popper"
  28. trigger="click"
  29. :virtual-ref="inputWidthRef"
  30. virtual-triggering
  31. >
  32. <template #default>
  33. <div class="icon-selector-warp">
  34. <div class="icon-selector-warp-title">{{ title }}</div>
  35. <el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
  36. <el-tab-pane lazy label="ali" name="ali">
  37. <IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
  38. </el-tab-pane>
  39. <el-tab-pane lazy label="ele" name="ele">
  40. <IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
  41. </el-tab-pane>
  42. <el-tab-pane lazy label="awe" name="awe">
  43. <IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
  44. </el-tab-pane>
  45. </el-tabs>
  46. </div>
  47. </template>
  48. </el-popover>
  49. </div>
  50. </template>
  51. <script setup lang="ts" name="iconSelector">
  52. import { defineAsyncComponent, ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
  53. import type { TabsPaneContext } from 'element-plus';
  54. import initIconfont from '/@/utils/getStyleSheets';
  55. import '/@/theme/iconSelector.scss';
  56. // 定义父组件传过来的值
  57. const props = defineProps({
  58. // 输入框前置内容
  59. prepend: {
  60. type: String,
  61. default: () => 'ele-Pointer',
  62. },
  63. // 输入框占位文本
  64. placeholder: {
  65. type: String,
  66. default: () => '请输入内容搜索图标或者选择图标',
  67. },
  68. // 输入框占位文本
  69. size: {
  70. type: String,
  71. default: () => 'default',
  72. },
  73. // 弹窗标题
  74. title: {
  75. type: String,
  76. default: () => '请选择图标',
  77. },
  78. // 禁用
  79. disabled: {
  80. type: Boolean,
  81. default: () => false,
  82. },
  83. // 是否可清空
  84. clearable: {
  85. type: Boolean,
  86. default: () => true,
  87. },
  88. // 自定义空状态描述文字
  89. emptyDescription: {
  90. type: String,
  91. default: () => '无相关图标',
  92. },
  93. // 双向绑定值,默认为 modelValue,
  94. // 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
  95. // 参考:https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
  96. modelValue: String,
  97. });
  98. // 定义子组件向父组件传值/事件
  99. const emit = defineEmits(['update:modelValue', 'get', 'clear']);
  100. // 引入组件
  101. const IconList = defineAsyncComponent(() => import('/@/components/iconSelector/list.vue'));
  102. // 定义变量内容
  103. const inputWidthRef = ref();
  104. const state = reactive({
  105. fontIconPrefix: '',
  106. fontIconWidth: 0,
  107. fontIconSearch: '',
  108. fontIconPlaceholder: '',
  109. fontIconTabActive: 'ali',
  110. fontIconList: {
  111. ali: [],
  112. ele: [],
  113. awe: [],
  114. },
  115. });
  116. // 处理 input 获取焦点时,modelValue 有值时,改变 input 的 placeholder 值
  117. const onIconFocus = () => {
  118. if (!props.modelValue) return false;
  119. state.fontIconSearch = '';
  120. state.fontIconPlaceholder = props.modelValue;
  121. };
  122. // 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
  123. const onIconBlur = () => {
  124. const list = fontIconTabNameList();
  125. setTimeout(() => {
  126. const icon = list.filter((icon: string) => icon === state.fontIconSearch);
  127. if (icon.length <= 0) state.fontIconSearch = '';
  128. }, 300);
  129. };
  130. // 图标搜索及图标数据显示
  131. const fontIconSheetsFilterList = computed(() => {
  132. const list = fontIconTabNameList();
  133. if (!state.fontIconSearch) return list;
  134. let search = state.fontIconSearch.trim().toLowerCase();
  135. return list.filter((item: string) => {
  136. if (item.toLowerCase().indexOf(search) !== -1) return item;
  137. });
  138. });
  139. // 根据 tab name 类型设置图标
  140. const fontIconTabNameList = () => {
  141. let iconList: any = [];
  142. if (state.fontIconTabActive === 'ali') iconList = state.fontIconList.ali;
  143. else if (state.fontIconTabActive === 'ele') iconList = state.fontIconList.ele;
  144. else if (state.fontIconTabActive === 'awe') iconList = state.fontIconList.awe;
  145. return iconList;
  146. };
  147. // 处理 icon 双向绑定数值回显
  148. const initModeValueEcho = () => {
  149. if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
  150. (<string | undefined>state.fontIconPlaceholder) = props.modelValue;
  151. (<string | undefined>state.fontIconPrefix) = props.modelValue;
  152. };
  153. // 处理 icon 类型,用于回显时,tab 高亮与初始化数据
  154. const initFontIconName = () => {
  155. let name = 'ali';
  156. if (props.modelValue!.indexOf('iconfont') > -1) name = 'ali';
  157. else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
  158. else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
  159. // 初始化 tab 高亮回显
  160. state.fontIconTabActive = name;
  161. return name;
  162. };
  163. // 初始化数据
  164. const initFontIconData = async (name: string) => {
  165. if (name === 'ali') {
  166. // 阿里字体图标使用 `iconfont xxx`
  167. if (state.fontIconList.ali.length > 0) return;
  168. await initIconfont.ali().then((res: any) => {
  169. state.fontIconList.ali = res.map((i: string) => `iconfont ${i}`);
  170. });
  171. } else if (name === 'ele') {
  172. // element plus 图标
  173. if (state.fontIconList.ele.length > 0) return;
  174. await initIconfont.ele().then((res: any) => {
  175. state.fontIconList.ele = res;
  176. });
  177. } else if (name === 'awe') {
  178. // fontawesome字体图标使用 `fa xxx`
  179. if (state.fontIconList.awe.length > 0) return;
  180. await initIconfont.awe().then((res: any) => {
  181. state.fontIconList.awe = res.map((i: string) => `fa ${i}`);
  182. });
  183. }
  184. // 初始化 input 的 placeholder
  185. // 参考(单项数据流):https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
  186. state.fontIconPlaceholder = props.placeholder;
  187. // 初始化双向绑定回显
  188. initModeValueEcho();
  189. };
  190. // 图标点击切换
  191. const onIconClick = (pane: TabsPaneContext) => {
  192. initFontIconData(pane.paneName as string);
  193. inputWidthRef.value.focus();
  194. };
  195. // 获取当前点击的 icon 图标
  196. const onColClick = (v: string) => {
  197. state.fontIconPlaceholder = v;
  198. state.fontIconPrefix = v;
  199. emit('get', state.fontIconPrefix);
  200. emit('update:modelValue', state.fontIconPrefix);
  201. inputWidthRef.value.focus();
  202. };
  203. // 清空当前点击的 icon 图标
  204. const onClearFontIcon = () => {
  205. state.fontIconPrefix = '';
  206. emit('clear', state.fontIconPrefix);
  207. emit('update:modelValue', state.fontIconPrefix);
  208. };
  209. // 获取 input 的宽度
  210. const getInputWidth = () => {
  211. nextTick(() => {
  212. state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
  213. });
  214. };
  215. // 监听页面宽度改变
  216. const initResize = () => {
  217. window.addEventListener('resize', () => {
  218. getInputWidth();
  219. });
  220. };
  221. // 页面加载时
  222. onMounted(() => {
  223. initFontIconData(initFontIconName());
  224. initResize();
  225. getInputWidth();
  226. });
  227. // 监听双向绑定 modelValue 的变化
  228. watch(
  229. () => props.modelValue,
  230. () => {
  231. initModeValueEcho();
  232. initFontIconName();
  233. }
  234. );
  235. </script>