ts
复制代码
<!--
* @description 标准unicode特殊字符输入框
* @see 参考:https://tools360.net/zh/subscript-generator
-->
<template>
<div class="special-char-input">
<el-input
v-bind="props"
:value="innerValue"
validateEvent
@blur="handleBlurInput"
@input="handleInputValue"
ref="inputRef"
>
</el-input>
<Transition name="slide-fade" mode="out-in">
<div
v-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-button
style="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">
<div
class="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>