vue + elementUI 实现特殊字符(上标、下标、特殊符号等)输入框
标准unicode特殊字符输入框
- 支持标准unicode特殊字符
- 输入框完全支持elmentUI input属性定义
- 在编辑位置插入选中字符,自动更新input光标
- 注意:并不是所有的数字和字母都有对应的上标和下标字符,当满足不了时需要寻找其他的解决方案,如:富文本输入框自定义上标、下标格式,可以满足所有需要的上标下标
上标、下标生成参考:https://tools360.net/zh/subscript-generator
1、组件定义
<!--
* @description 标准unicode特殊字符输入框
* @see 参考:https://tools360.net/zh/subscript-generator
-->
<template><div class="special-char-input"><el-inputv-bind="props":value="innerValue"validateEvent@blur="handleBlurInput"@input="handleInputValue"ref="inputRef"></el-input><Transition name="slide-fade" mode="out-in"><divv-if="!panelVisible"style="text-align: right;line-height: 1;padding-right: 10px;width: 100%;position: absolute;"key="link"><el-link icon="el-icon-edit" @click="handleShowPanel" style="padding: 5px">特殊字符库</el-link></div><div v-if="panelVisible" class="special-char-panel"><el-card:body-style="{ 'padding-top': '10px', 'padding-bottom': '5px' }"style="border-color: #dcdfe6; margin-bottom: 5px"key="panel"><div class="title-box"><div class="title-txt"><i class="el-icon-edit" style="margin-right: 5px"></i>特殊字符库</div><div class="title-action"><el-buttonstyle="padding: 0"type="text"icon="el-icon-caret-top"@click="handleHidePanel">收起</el-button></div></div><el-tabs tab-position="left" v-model="activeTab" @tab-click="handleTabClick"><el-tab-pane v-for="(tab, idx) in tabs" :key="idx" :label="tab.label" :name="tab.key"><div class="char-content"><divclass="char-item"v-for="(char, idx) in getTabChars(tab.key)":key="idx"@click="handleClickChar(char)":title="`${tab.label}: ${char}`":style="{ 'font-size': tab.fontSize || '17px' }">{{ char }}</div></div></el-tab-pane></el-tabs></el-card></div></Transition></div>
</template><script setup lang="ts">
import { ElementUIComponentSize } from 'element-ui/types/component';
import type { AutoSize, ElInput, InputType, Resizability } from 'element-ui/types/input';
import { computed, nextTick, watch, watchEffect } from 'vue';
import { getCurrentInstance } from 'vue';
import { h, onMounted, ref, unref } from 'vue';/**
* @description 无法直接使用ElInput,因为 class是运行时,此处需要静态声明
*/
type Props = {/** Type of input */type?: InputType;/** Binding value */value?: string | number;/** Maximum Input text length */maxlength?: number;/** Minimum Input text length */minlength?: number;/** Placeholder of Input */placeholder?: string;/** Whether Input is disabled */disabled?: boolean;/** Size of Input, works when type is not 'textarea' */size?: ElementUIComponentSize;/** Prefix icon class */prefixIcon?: string;/** Suffix icon class */suffixIcon?: string;/** Number of rows of textarea, only works when type is 'textarea' */rows?: number;/** Whether textarea has an adaptive height, only works when type is 'textarea' */autosize?: boolean | Partial<AutoSize>;/** @Deprecated in next major version */autoComplete?: string;/** Same as autocomplete in native input */autocomplete?: string;/** Same as name in native input */name?: string;/** Same as readonly in native input */readonly?: boolean;/** Same as max in native input */max?: any;/** Same as min in native input */min?: any;/** Same as step in native input */step?: any;/** Control the resizability */resize?: Resizability;/** Same as autofocus in native input */autofocus?: boolean;/** Same as form in native input */form?: string;/** Whether to trigger form validatio */validateEvent?: boolean;/** Whether the input is clearable */clearable?: boolean;/** Whether to show password */showPassword?: boolean;/** Whether to show wordCount when setting maxLength */showWordLimit?: boolean;
};const props = defineProps<Props>();
const emits = defineEmits<{(e: 'input', val: string): void;
}>();
const inputRef = ref<ElInput>();
/** 选择面板可见 */
const panelVisible = ref(false);
/** 输入框失焦时的索引 */
const blurIndex = ref<number>();
/** 输入框值 */
const innerValue = ref<string>();// prettier-ignore
const charSets:{[key:string]:string[]} = {// 下标分组subscript: ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉','₊', '₋', '₌', '₍', '₎','ₐ', 'ₑ' ,'ₕ', 'ᵢ', 'ⱼ', 'ₖ', 'ₗ', 'ₘ', 'ₙ', 'ₒ','ₚ', 'ᵣ', 'ₛ', 'ₜ', 'ᵤ', 'ᵥ', 'ₓ','ₔ', 'ᵦ', '𝑔','ᵧ', 'ᵨ', 'ᵩ', 'ᵪ',],// 上标分组superscript: ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹','⁺', '⁻', '⁼', '⁽', '⁾', 'ᵃ', 'ᵇ', 'ᶜ', 'ᵈ', 'ᵉ', 'ᶠ', 'ᵍ', 'ʰ', 'ⁱ', 'ʲ', 'ᵏ', 'ˡ', 'ᵐ', 'ⁿ', 'ᵒ', 'ᵖ', 'ʳ', 'ˢ', 'ᵗ', 'ᵘ', 'ᵛ', 'ʷ', 'ˣ', 'ʸ', 'ᶻ','ᴬ', 'ᴮ', 'ᴰ', 'ᴱ', 'ᴳ', 'ᴴ', 'ᴵ', 'ᴶ', 'ᴷ', 'ᴸ', 'ᴹ', 'ᴺ', 'ᴼ', 'ᴾ', 'ᴿ', 'ᵀ', 'ᵁ', 'ⱽ', 'ᵂ','ᵝ', 'ᵞ', 'ᵟ', 'ᵋ', 'ᶿ', 'ᵠ', 'ᵡ',],// 罗马数字roman: ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩','⑪', '⑫', '⑬', '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳','ⅰ', 'ⅱ', 'ⅲ', 'ⅳ', 'ⅴ', 'ⅵ', 'ⅶ', 'ⅷ', 'ⅸ', 'ⅹ','ⅺ', 'ⅻ','Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ', 'Ⅵ', 'Ⅶ', 'Ⅷ', 'Ⅸ', 'Ⅹ','Ⅺ', 'Ⅻ', ],// 希腊字母letter: ['α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ','λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ','φ', 'χ', 'ψ', 'ω','Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ', 'Ι', 'Κ','Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π', 'Ρ', 'Σ', 'Τ', 'Υ','Φ', 'Χ', 'Ψ', 'Ω'],// 特殊符号symbol: ['℃', '℉', '°', '′', '″', '±', '×', '÷', '√', '∞', '≈', '≠', '≡', '≤', '≥', '≦', '≧', '≨', '≩', '™', '®', '©', '℠', '℗', '℡', '∮','∯', '∰', '∱', '∲', '∳', '∫', '∑', '∏', '∂', '∆','∈', '∉', '∋', '∌','⊂', '⊃', '⊄', '⊅', '⊆', '⊇', '⊈', '⊉', '⊊','⊋', ]/* math:['±', '×', '÷', '√', '∞', '∫', '∑', '∏', '∂', '∆','≈', '≠', '≡', '≤', '≥', '∈', '∉', '∋', '∌', '⊕','⊗', '⊥', '∥', '∠', '∟', '∘', '∙', '⋆', '★', '☆','✔', '✕', '◯', '□', '△', '▷', '◁', '◇', '○', '●','◆', '■', '▲', '▼', '►', '◄', '●', '◦', '‣', '♦','♥', '♠', '♣', '✓', '✔', '✗', '✘', '∝', '∅', '∇','¬', '∧', '∨', '∩', '∪', '∴', '∵', '∶', '∷', '∼','∽', '≃', '≅', '≇', '≉', '≊', '≋', '≌', '≍', '≎','≏', '≐', '≑', '≒', '≓', '≔', '≕', '≖', '≗', '≘','≙', '≚', '≛', '≜', '≝', '≞', '≟', '≠', '≡', '≢','≣', '≤', '≥', '≦', '≧', '≨', '≩', '≪', '≫', '≬','≭', '≮', '≯', '≰', '≱', '≲', '≳', '≴', '≵', '≶','≷', '≸', '≹', '≺', '≻', '≼', '≽', '≾', '≿', '⊀','⊁', '⊂', '⊃', '⊄', '⊅', '⊆', '⊇', '⊈', '⊉', '⊊','⊋', '⊌', '⊍', '⊎', '⊏', '⊐', '⊑', '⊒', '⊓', '⊔'], *//* symbol: ['℃', '℉', '°', '′', '″', 'ℏ', 'Å', 'µ', 'Ω', 'Φ','Ψ', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι','κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ','υ', 'φ', 'χ', 'ψ', 'ω', 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ','Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω', '∇', '∂', '∫', '∮','∯', '∰', '∱', '∲', '∳', '⊥', '∥', '∠', '∡', '∢','⊾', '⊿', '⋔', '⋕', '⋖', '⋗', '⋘', '⋙', '⋚', '⋛','⋜', '⋝', '⋞', '⋟', '⋠', '⋡', '⋢', '⋣', '⋤', '⋥','⋦', '⋧', '⋨', '⋩', '⋪', '⋫', '⋬', '⋭', '⋮', '⋯','⋰', '⋱', '⋲', '⋳', '⋴', '⋵', '⋶', '⋷', '⋸', '⋹'] */
};// 标签列表
const tabs = [{ key: 'superscript', label: '上标', fontSize: '21px' },{ key: 'subscript', label: '下标', fontSize: '21px' },{ key: 'roman', label: '数字序号', fontSize: '17px' },{ key: 'letter', label: '希腊字母', fontSize: '17px' },{ key: 'symbol', label: '特殊符号', fontSize: '17px' },
];
// 当前激活标签
const activeTab = ref<string>(tabs[0].key);/** 获取tab的字符集 */
const getTabChars = (tabKey: string) => {const chars = charSets[tabKey] || [];// chars = chars.sort((a, b) => a.codePointAt(0)! - b.codePointAt(0)!);return Array.from(new Set(chars));
};/** 更新输入框光标位置 */
const updateInputCursorPosition = async () => {await nextTick();inputRef.value?.focus();const inputEl =inputRef.value?.$el.querySelector('textarea') || inputRef.value?.$el.querySelector('input');const index = blurIndex.value ?? innerValue.value?.length ?? 0;if (inputEl) {inputEl.setSelectionRange(index, index);}
};/** 输入框内容变更 */
const handleInputValue = (value: string) => {innerValue.value = value;emits('input', value);
};/** 输入框失焦 */
const handleBlurInput = (e: FocusEvent) => {blurIndex.value = (e.target as HTMLInputElement)?.selectionStart || 0;
};/** 显示面板 */
const handleShowPanel = (e: Event) => {panelVisible.value = true;updateInputCursorPosition();
};/** 收起面板 */
const handleHidePanel = (e: Event) => {panelVisible.value = false;
};/** 切换tab */
const handleTabClick = () => {updateInputCursorPosition();
};/** 选中字符 */
const handleClickChar = (char: string) => {const index = blurIndex.value ?? 0;const str = innerValue.value || '';const newValue = str.slice(0, index) + char + str.slice(index);innerValue.value = newValue;blurIndex.value = index + char.length;// 触发input事件emits('input', newValue);// 更新光标位置updateInputCursorPosition();
};watch(() => props.value,(val) => {if (val !== innerValue.value) {innerValue.value = typeof val === 'number' ? val.toString() : val;}},{ deep: true, immediate: true },
);
</script><style scoped lang="scss">
.special-char-input {position: relative;padding-bottom: 5px;.special-char-panel {width: 100%;// position: absolute;z-index: 999;.title-box {display: flex;align-items: center;.title-txt {font-size: 15px;font-weight: bold;flex: 1;}.title-action {}}.char-content {padding: 16px;display: flex;flex-wrap: wrap;gap: 8px;max-height: 300px;overflow-y: auto;.char-item {color: #333;cursor: pointer;border: 1px solid #dcdfe6;border-radius: 4px;transition: all 0.3s;width: 35px;height: 35px;text-align: center;line-height: 35px;/* font-family:'Microsoft YaHei', 微软雅黑, STHei, 华文黑体, 'Helvetica Neue', Helvetica, Arial,sans-serif; */&:hover {background: #f5f7fa;border-color: #409eff;color: #409eff;}}}}
}.slide-fade-enter-active {transition: all 0.2s ease;
}
.slide-fade-leave-active {transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter,
.slide-fade-leave-to {transform: translateX(0) translateY(-10%);opacity: 0;
}
</style>
2、组件使用
<SpecialCharInputv-model="formInfo.scopeLimit"type="textarea"placeholder="请输入限制范围":autosize="{ minRows: 4 }":maxlength="2000"show-word-limit></SpecialCharInput>
