<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 3 示例</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
.container {
margin: 50px auto;
padding: 20px;
display: flex;
}
.left {
width: 50%;
}
.form-group {
margin-bottom: 20px;
}
.message-group {
font-size: 14px;
padding: 0 5px 5px;
border: 1px solid rgba(0, 0, 0, 0.15);
flex: 1;
}
.params {
display: flex;
flex: 1;
align-items: center;
justify-content: flex-start;
cursor: pointer;
flex-wrap: wrap;
}
.param {
word-break: keep-all;
margin-right: 25px;
color: #1890ff;
border: 1px dashed #1890ff;
border-radius: 4px;
padding: 2px 5px;
span {
padding-right: 5px;
}
}
.line {
width: 100%;
height: 1px;
background-color: #ddd;
margin: 10px 0;
}
.form-template {
width: 100%;
min-height: 100px;
box-sizing: border-box;
font-size: 14px;
padding: 10px;
line-height: 1.5;
word-break: break-word;
resize: vertical;
overflow: auto;
}
smstag {
color: #1890ff;
border-radius: 4px;
padding: 1px 1px 1px 8px;
border: 1px solid #1890ff;
white-space: nowrap;
margin: 0 3px;
font-weight: 400;
cursor: default;
font-size: 14px;
}
.deleteBtn {
color: #1890ff;
cursor: pointer;
margin-left: 5px;
margin-right: 0px;
font-size: 14px;
height: 27px;
display: inline-block;
vertical-align: middle;
}
label {
display: block;
margin-bottom: 5px;
}
select,
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="left">
<div class="form-group">
<label>业务场景:</label>
<select v-model="selectedTemplate" @change="handleChange">
<option value="" disabled hidden>请选择业务场景</option>
<option v-for="scene in scenes" :key="scene.value" :value="scene">
{{ scene.label }}
</option>
</select>
</div>
<div class="form-group message-group">
<label>短信模板:</label>
<div class="params">
业务参数:
<div
class="param"
v-for="param in selectedTemplate.value"
:key="param"
@click="insertStr(param)"
>
{{ param }}
</div>
</div>
<div class="line"></div>
<div
id="messageId"
class="form-template"
contenteditable="true"
type="text"
ref="messageRef"
@click="handleClick"
></div>
</div>
</div>
<div class="right">
<div>结果:</div>
<div>{{ messageResult }}</div>
</div>
</div>
</div>
<script>
const { createApp, ref, watch, onMounted, onUnmounted } = Vue
createApp({
setup() {
const scenes = ref([
{
label: '用户注册短信验证码',
value: ['短信验证码'],
content:
'尊敬的用户您好,您正在注册账号,验证码为 {1},请勿泄露给他人,有效期5分钟',
params: ['短信验证码']
},
{
label: '修改绑定手机号',
value: ['短信验证码', '时间'],
content:
'尊敬的用户您好,您正在变更手机号,验证码为 {1} ,请勿泄露给他人,有效期 {2} 分钟。',
params: ['短信验证码', '时间']
}
])
const selectedTemplate = ref('')
const messageRef = ref(null)
const messageResult = ref('')
// 光标位置
const savedRange = ref(null)
// select 选择事件
function handleChange(value) {
messageRef.value.innerHTML = replaceTemplateParams()
getMessageContent(messageRef.value.innerHTML)
}
// 替换模板参数
function replaceTemplateParams() {
const regx = /\{(.*?)\}/g
let tempContent = selectedTemplate.value.content
return tempContent.replace(regx, match => {
let tempValue = ''
let index = parseInt(match.replace(/\{|\}/g, ''))
let tempParam = ''
tempParam = selectedTemplate.value.params[index - 1]
let node = document.createElement('smstag')
node.contentEditable = 'false'
// 添加删除按钮
let deleteBtn = document.createElement('span')
deleteBtn.innerText = 'x'
// 删除按钮添加类名
deleteBtn.className = 'deleteBtn'
node.innerText = tempParam
node.appendChild(deleteBtn)
let space = document.createTextNode('\u00A0')
node.appendChild(space)
return node.outerHTML
})
}
// message 内容
function getMessageContent(content) {
let result = ''
const regex = /<smstag.*?>(.*?)<\/smstag>/g
result = content.replace(regex, (match, result) => {
let matchStr = result.replace(/<\/?span.*?>|x| /g, '')
return `【${matchStr}】`
})
messageResult.value = result
}
function handleClick(e) {
if (e.target.nodeName === 'SPAN') {
e.target.parentNode.remove()
getMessageContent(messageRef.value.innerHTML)
}
}
// 生成插入内容
function insertStr(str) {
let node = document.createElement('smstag')
node.innerText = str
node.contentEditable = 'false'
// 添加删除按钮
let deleteBtn = document.createElement('span')
deleteBtn.innerText = 'x'
// 删除按钮添加类名
deleteBtn.className = 'deleteBtn'
node.appendChild(deleteBtn)
let space = document.createTextNode('\u00A0')
node.appendChild(space)
insertNode(node)
}
// 插入到光标位置
function insertNode(node) {
// 删除选中内容
savedRange.value && savedRange.value.deleteContents()
// 插入标签
savedRange.value && savedRange.value.insertNode(node)
// 插入成功,光标定位到标签后面
savedRange.value && savedRange.value.setStartAfter(node.lastChild)
getMessageContent(messageRef.value.innerHTML)
// 光标置空
savedRange.value = null
}
function selectionChangeFn() {
let sel = window.getSelection()
let range = sel.rangeCount > 0 ? sel.getRangeAt(0) : null
if (
range &&
range.commonAncestorContainer.ownerDocument.activeElement.id ===
'messageId'
) {
savedRange.value = range
}
}
onMounted(() => {
document.addEventListener('selectionchange', selectionChangeFn)
})
onUnmounted(() => {
document.removeEventListener('selectionchange', selectionChangeFn)
})
return {
scenes,
selectedTemplate,
messageRef,
messageResult,
handleChange,
insertStr,
handleClick
}
}
}).mount('#app')
</script>
</body>
</html>