feat(编辑器): 优化移动端编辑器交互体验
- 修复键盘弹出时底部操作栏遮挡问题,添加固定定位效果 - 改进光标定位逻辑,适配不同输入场景 - 增加内容预填充功能,便于测试和演示 - 调整底部间距和动画高度,适配不同设备 - 添加vConsole调试工具便于移动端调试
This commit is contained in:
159
2.html
Normal file
159
2.html
Normal file
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>可点击其他元素+保持光标不丢</title>
|
||||
<style>
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 可编辑区域 */
|
||||
.edit-area {
|
||||
min-height: 100px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 其他可点击元素(都能正常触发点击逻辑) */
|
||||
.other-btn {
|
||||
margin: 0 8px 10px 0;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #4285f4;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.link {
|
||||
color: #4285f4;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.popup-box {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
#popup {
|
||||
display: none;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ff4444;
|
||||
background: #fff0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 核心:可编辑区域 -->
|
||||
<div class="edit-area" contenteditable="true" id="editArea">
|
||||
点击这里输入...点击下方按钮/链接/区域,它们能正常工作,光标也不会消失
|
||||
</div>
|
||||
|
||||
<!-- 其他可点击元素(都能正常触发逻辑) -->
|
||||
<button class="other-btn" id="logBtn">点击打印日志</button>
|
||||
<button class="other-btn" id="insertBtn">点击插入文本到编辑区</button>
|
||||
<div class="link" id="jumpLink">点击模拟跳转(不会真跳)</div>
|
||||
<div class="popup-box" id="showPopupBtn">点击显示弹窗</div>
|
||||
<div id="popup">这是弹窗内容(点击其他地方不会关闭,除非加逻辑)</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 1. 获取核心元素
|
||||
const editArea = document.getElementById('editArea');
|
||||
const popup = document.getElementById('popup');
|
||||
let lastRange = null; // 存储光标位置,用于恢复
|
||||
|
||||
// 2. 初始化:自动聚焦+光标定位到末尾
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
editArea.focus();
|
||||
moveCursorToEnd(editArea);
|
||||
});
|
||||
|
||||
// 3. 关键:监听可编辑区域的blur事件(即将失焦时)
|
||||
editArea.addEventListener('blur', () => {
|
||||
// 延迟10ms重新聚焦(让其他元素的点击逻辑先执行,避免冲突)
|
||||
setTimeout(() => {
|
||||
if (lastRange) { // 恢复之前的光标位置
|
||||
editArea.focus();
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastRange);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// 4. 实时保存光标位置(确保失焦后能恢复)
|
||||
editArea.addEventListener('selectionchange', () => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount > 0) {
|
||||
// 克隆光标范围(避免原对象被覆盖)
|
||||
lastRange = selection.getRangeAt(0).cloneRange();
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------- 以下是其他元素的正常点击逻辑(可自定义) ----------------------
|
||||
// 按钮1:打印日志(正常触发)
|
||||
document.getElementById('logBtn').addEventListener('click', () => {
|
||||
console.log('按钮被点击了!当前编辑区内容:', editArea.innerText);
|
||||
alert('按钮点击成功!光标还在编辑区哦~');
|
||||
});
|
||||
|
||||
// 按钮2:插入文本到编辑区(正常触发+光标同步)
|
||||
document.getElementById('insertBtn').addEventListener('click', () => {
|
||||
const insertText = '【插入的文本】';
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
// 插入文本(不影响原光标位置)
|
||||
range.deleteContents();
|
||||
const textNode = document.createTextNode(insertText);
|
||||
range.insertNode(textNode);
|
||||
|
||||
// 光标移到插入文本末尾
|
||||
range.setStartAfter(textNode);
|
||||
range.setEndAfter(textNode);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
// 确保编辑区仍有焦点(双重保险)
|
||||
editArea.focus();
|
||||
});
|
||||
|
||||
// 链接:模拟跳转(正常触发,不真跳转)
|
||||
document.getElementById('jumpLink').addEventListener('click', () => {
|
||||
alert('模拟跳转逻辑触发!(实际不会跳转,光标还在编辑区)');
|
||||
});
|
||||
|
||||
// 区域:显示弹窗(正常触发)
|
||||
document.getElementById('showPopupBtn').addEventListener('click', () => {
|
||||
popup.style.display = popup.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// ---------------------- 辅助函数 ----------------------
|
||||
// 光标定位到编辑区末尾
|
||||
function moveCursorToEnd(element) {
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
range.selectNodeContents(element);
|
||||
range.collapse(false); // false=末尾,true=开头
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,8 +48,7 @@ editor {
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 3.2rem;
|
||||
padding-bottom: 8.6667rem;
|
||||
padding-bottom: 300px;
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
.container .title-box {
|
||||
@@ -81,7 +80,7 @@ editor {
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.container .editor-box .editor {
|
||||
margin: 0.4rem;
|
||||
margin: 0.4rem 0.4rem 0.2rem;
|
||||
width: 8.6rem;
|
||||
min-height: 10rem;
|
||||
border: none;
|
||||
@@ -121,7 +120,24 @@ editor {
|
||||
height: 1px;
|
||||
display: inline-block;
|
||||
}
|
||||
.container .editor-box .label {
|
||||
.container .editor-box .operate-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0.2rem;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
.container .editor-box .operate-box.fixed {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
background-color: #fff;
|
||||
z-index: 999;
|
||||
}
|
||||
.container .editor-box .operate-box.fixed .label {
|
||||
width: 100vw;
|
||||
}
|
||||
.container .editor-box .operate-box .label {
|
||||
white-space: nowrap;
|
||||
/* height: 0.84rem; */
|
||||
margin-bottom: 0.2rem;
|
||||
@@ -129,7 +145,7 @@ editor {
|
||||
overflow: auto;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
.container .editor-box .label .item {
|
||||
.container .editor-box .operate-box .label .item {
|
||||
width: fit-content;
|
||||
height: 0.64rem;
|
||||
line-height: 0.64rem;
|
||||
@@ -140,19 +156,19 @@ editor {
|
||||
padding: 0 0.24rem;
|
||||
display: inline-flex;
|
||||
}
|
||||
.container .editor-box .label .item:not(:last-of-type) {
|
||||
.container .editor-box .operate-box .label .item:not(:last-of-type) {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
.container .editor-box .label .item:first-of-type {
|
||||
.container .editor-box .operate-box .label .item:first-of-type {
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
.container .editor-box .label .item:last-of-type {
|
||||
.container .editor-box .operate-box .label .item:last-of-type {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
.container .editor-box .btn-list {
|
||||
.container .editor-box .operate-box .btn-list {
|
||||
margin: 0 0.4rem 0.4rem;
|
||||
}
|
||||
.container .editor-box .btn-list .item {
|
||||
.container .editor-box .operate-box .btn-list .item {
|
||||
width: fit-content;
|
||||
height: 0.72rem;
|
||||
line-height: 0.72rem;
|
||||
@@ -166,15 +182,15 @@ editor {
|
||||
margin-right: 0.2rem;
|
||||
position: relative;
|
||||
}
|
||||
.container .editor-box .btn-list .item.pitch {
|
||||
.container .editor-box .operate-box .btn-list .item.pitch {
|
||||
background-color: #f6f6bd;
|
||||
}
|
||||
.container .editor-box .btn-list .item .icon {
|
||||
.container .editor-box .operate-box .btn-list .item .icon {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
margin-right: 0.14rem;
|
||||
}
|
||||
.container .editor-box .btn-list .item .file {
|
||||
.container .editor-box .operate-box .btn-list .item .file {
|
||||
opacity: 0;
|
||||
/* 隐藏输入框 */
|
||||
background: transparent;
|
||||
@@ -186,7 +202,7 @@ editor {
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.container .editor-box .btn-list .item .file::after {
|
||||
.container .editor-box .operate-box .btn-list .item .file::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -194,14 +210,14 @@ editor {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.container .editor-box .btn-list .unfold {
|
||||
.container .editor-box .operate-box .btn-list .unfold {
|
||||
width: 0.72rem;
|
||||
height: 0.72rem;
|
||||
background-color: #f6f6f6;
|
||||
border: 0.0133rem solid #ebebeb;
|
||||
border-radius: 1.4667rem;
|
||||
}
|
||||
.container .editor-box .btn-list .unfold .icon {
|
||||
.container .editor-box .operate-box .btn-list .unfold .icon {
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
}
|
||||
@@ -292,7 +308,7 @@ editor {
|
||||
height: 0;
|
||||
}
|
||||
100% {
|
||||
height: 8.6667rem;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
.container .pop .emoji-system-list .item {
|
||||
|
||||
185
css/index.less
185
css/index.less
@@ -57,8 +57,8 @@ editor {
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: rgba(245, 245, 245, 1);
|
||||
padding-bottom: 3.2rem;
|
||||
padding-bottom: 8.6667rem;
|
||||
padding-bottom: 300px;
|
||||
// padding-bottom: 8.6667rem;
|
||||
|
||||
padding-top: 0.4rem;
|
||||
|
||||
@@ -93,7 +93,7 @@ editor {
|
||||
border-radius: 0.4rem;
|
||||
|
||||
.editor {
|
||||
margin: 0.4rem;
|
||||
margin: 0.4rem 0.4rem 0.2rem;
|
||||
width: 8.6rem;
|
||||
min-height: 10rem;
|
||||
border: none;
|
||||
@@ -146,98 +146,117 @@ editor {
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
/* height: 0.84rem; */
|
||||
margin-bottom: 0.2rem;
|
||||
width: 9.4rem;
|
||||
overflow: auto;
|
||||
padding-bottom: 0.2rem;
|
||||
.operate-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0.2rem;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
.item {
|
||||
width: fit-content;
|
||||
height: 0.64rem;
|
||||
line-height: 0.64rem;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border-radius: 1.46rem;
|
||||
font-size: 0.28rem;
|
||||
color: #606060;
|
||||
padding: 0 0.24rem;
|
||||
display: inline-flex;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0.4rem;
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
background-color: #fff;
|
||||
z-index: 999;
|
||||
.label {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-list {
|
||||
margin: 0 0.4rem 0.4rem;
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
/* height: 0.84rem; */
|
||||
margin-bottom: 0.2rem;
|
||||
width: 9.4rem;
|
||||
overflow: auto;
|
||||
padding-bottom: 0.2rem;
|
||||
|
||||
.item {
|
||||
width: fit-content;
|
||||
height: 0.72rem;
|
||||
line-height: 0.72rem;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border: 0.0133rem solid rgba(235, 235, 235, 1);
|
||||
border-radius: 1.46rem;
|
||||
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||
font-size: 0.32rem;
|
||||
color: #555555;
|
||||
padding: 0 0.24rem;
|
||||
margin-right: 0.2rem;
|
||||
position: relative;
|
||||
.item {
|
||||
width: fit-content;
|
||||
height: 0.64rem;
|
||||
line-height: 0.64rem;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border-radius: 1.46rem;
|
||||
font-size: 0.28rem;
|
||||
color: #606060;
|
||||
padding: 0 0.24rem;
|
||||
display: inline-flex;
|
||||
|
||||
&.pitch {
|
||||
background-color: rgba(246, 246, 189, 1);
|
||||
}
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
margin-right: 0.14rem;
|
||||
}
|
||||
&:first-of-type {
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.file {
|
||||
opacity: 0; /* 隐藏输入框 */
|
||||
background: transparent;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
&:last-of-type {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unfold {
|
||||
width: 0.72rem;
|
||||
height: 0.72rem;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border: 0.0133rem solid rgba(235, 235, 235, 1);
|
||||
border-radius: 1.4667rem;
|
||||
.btn-list {
|
||||
margin: 0 0.4rem 0.4rem;
|
||||
|
||||
.icon {
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
.item {
|
||||
width: fit-content;
|
||||
height: 0.72rem;
|
||||
line-height: 0.72rem;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border: 0.0133rem solid rgba(235, 235, 235, 1);
|
||||
border-radius: 1.46rem;
|
||||
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||
font-size: 0.32rem;
|
||||
color: #555555;
|
||||
padding: 0 0.24rem;
|
||||
margin-right: 0.2rem;
|
||||
position: relative;
|
||||
|
||||
&.pitch {
|
||||
background-color: rgba(246, 246, 189, 1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
margin-right: 0.14rem;
|
||||
}
|
||||
|
||||
.file {
|
||||
opacity: 0; /* 隐藏输入框 */
|
||||
background: transparent;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unfold {
|
||||
width: 0.72rem;
|
||||
height: 0.72rem;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border: 0.0133rem solid rgba(235, 235, 235, 1);
|
||||
border-radius: 1.4667rem;
|
||||
|
||||
.icon {
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,7 +360,7 @@ editor {
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 8.6667rem;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
index.html
68
index.html
@@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="./css/index.css" />
|
||||
<script src="./js/vue.global.js"></script>
|
||||
|
||||
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script> -->
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="appIndex">
|
||||
@@ -16,33 +16,33 @@
|
||||
<textarea class="input" id="title" placeholder="输入标题(非必填)" :maxlength="titleLength" v-model="title" ref="titleTextarea" @input="adjustTextareaHeight"></textarea>
|
||||
</div>
|
||||
<div class="editor-box">
|
||||
<div class="editor" :class="{ 'empty': isEmpty }" ref="editorRef" id="editor" placeholder="输入正文" contenteditable="true" @input="onEditorInput">
|
||||
森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅vv
|
||||
</div>
|
||||
<div class="editor" :class="{ 'empty': isEmpty }" ref="editorRef" id="editor" placeholder="输入正文" contenteditable="true" @input="onEditorInput" @focus="onEditorFocus" @blur="onEditorBlur" v-html="info.content"></div>
|
||||
|
||||
<div class="label flexflex" scroll-x>
|
||||
<div class="item" v-for="index in 8" :key="index" @click="insertLabel(`推荐标签${index}`)">#推荐标签{{index}}</div>
|
||||
</div>
|
||||
<div class="operate-box" :class="{'fixed': fixedState}" ref="operateRef" contenteditable="false" @click="fixedState = false">
|
||||
<div class="label flexflex" scroll-x>
|
||||
<div class="item" v-for="index in 8" :key="index" @click.stop="insertLabel(`推荐标签${index}`)">#推荐标签{{ index }}</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-list flexacenter">
|
||||
<div class="item flexacenter" :class="{ 'pitch': isPTitle }" @click="paragraphTitle">
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/T-icon.png" />
|
||||
<div class="text">段落标题</div>
|
||||
</div>
|
||||
<div class="item flexacenter">
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/img-icon.png" />
|
||||
<div class="text">图片</div>
|
||||
<input class="file" type="file" @change="insertImage" accept=".png, .jpg, .jpeg" />
|
||||
</div>
|
||||
<div class="item flexacenter" @click="openEmoji">
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/smiling-face-round-black.png" />
|
||||
<div class="text">表情</div>
|
||||
</div>
|
||||
<div class="flex1"></div>
|
||||
<!-- <div class="unfold flexcenter">
|
||||
<div class="btn-list flexacenter">
|
||||
<div class="item flexacenter" :class="{ 'pitch': isPTitle }" @click="paragraphTitle">
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/T-icon.png" />
|
||||
<div class="text">段落标题</div>
|
||||
</div>
|
||||
<div class="item flexacenter">
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/img-icon.png" />
|
||||
<div class="text">图片</div>
|
||||
<input class="file" type="file" @change="insertImage" accept=".png, .jpg, .jpeg" />
|
||||
</div>
|
||||
<div class="item flexacenter" @click="openEmoji">
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/smiling-face-round-black.png" />
|
||||
<div class="text">表情</div>
|
||||
</div>
|
||||
<div class="flex1"></div>
|
||||
<!-- <div class="unfold flexcenter">
|
||||
https://app.gter.net/image/miniApp/offer/fold-icon.png
|
||||
<img class="icon" src="https://app.gter.net/image/miniApp/offer/unfold-icon.png" />
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,15 +52,15 @@
|
||||
<div class="text">匿名发布</div>
|
||||
</div>
|
||||
|
||||
<!-- <cover-view class="new-footer-btn">
|
||||
<cover-view view class="flexacenter save-draft-btn" bindtap="handleLastPublish" data-state="0">
|
||||
<cover-view class="flexcenter save-draft-icon-box">
|
||||
<cover-image class="save-draft-icon" mode="widthFix" src="https://app.gter.net/image/miniApp/offer/draft-icon.png"></cover-image>
|
||||
</cover-view>
|
||||
<cover-view>存草稿</cover-view>
|
||||
</cover-view>
|
||||
<cover-view class="new-footer-submit flex1 flexcenter" bindtap="authenticationSubmit">发布</cover-view>
|
||||
</cover-view> -->
|
||||
<div class="new-footer-btn">
|
||||
<div view class="flexacenter save-draft-btn" bindtap="handleLastPublish" data-state="0">
|
||||
<div class="flexcenter save-draft-icon-box">
|
||||
<img class="save-draft-icon" src="https://app.gter.net/image/miniApp/offer/draft-icon.png"></img>
|
||||
</div>
|
||||
<div>存草稿</div>
|
||||
</div>
|
||||
<div class="new-footer-submit flex1 flexcenter" bindtap="authenticationSubmit">发布</div>
|
||||
</div>
|
||||
|
||||
<div class="pop flexflex" v-if="emojiState" @click="closeEmoji">
|
||||
<div class="emoji-system-list" scroll-y="true" show-scrollbar="false" enhanced="true">
|
||||
@@ -70,12 +70,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <script>
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
console.log("Hello world");
|
||||
|
||||
vConsole.destroy();
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<script src="./js/fontSize.js"></script>
|
||||
<script src="./js/index.js"></script>
|
||||
|
||||
75
js/index.js
75
js/index.js
@@ -10,6 +10,7 @@ createApp({
|
||||
|
||||
let info = ref({
|
||||
anonymity: 0,
|
||||
content: "森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅vv",
|
||||
});
|
||||
|
||||
const titleTextarea = ref(null);
|
||||
@@ -33,6 +34,8 @@ createApp({
|
||||
document.addEventListener("selectionchange", getFocusedNodeName);
|
||||
// 添加键盘事件监听
|
||||
document.addEventListener("keydown", handleDeleteKey);
|
||||
|
||||
judgeIsEmpty();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -259,6 +262,8 @@ createApp({
|
||||
|
||||
// 将范围添加到选择对象,不设置焦点
|
||||
selection.addRange(range);
|
||||
|
||||
editorRef.value.blur();
|
||||
};
|
||||
|
||||
let isEmpty = ref(true);
|
||||
@@ -273,47 +278,52 @@ createApp({
|
||||
}
|
||||
|
||||
judgeIsEmpty();
|
||||
|
||||
getCursorPosition("text");
|
||||
};
|
||||
|
||||
const fixedState = ref(false);
|
||||
const onEditorFocus = () => {
|
||||
setTimeout(() => getKeyboardHeight(), 500);
|
||||
};
|
||||
|
||||
const onEditorBlur = () => {
|
||||
// fixedState.value = false;
|
||||
};
|
||||
|
||||
// 判断是否为空
|
||||
const judgeIsEmpty = () => {
|
||||
const text = editorRef.value.innerText;
|
||||
console.log("text", text);
|
||||
|
||||
isEmpty.value = text.length == 0 && !editorRef.value.querySelector("img");
|
||||
};
|
||||
|
||||
const paragraphTitle = () => {
|
||||
// 保存当前滚动位置
|
||||
const scrollTop = window.scrollY;
|
||||
|
||||
|
||||
editorRef.value.focus();
|
||||
if (!lastSelection) return;
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastSelection);
|
||||
|
||||
|
||||
// 使用try-catch确保即使命令执行失败也能恢复滚动位置
|
||||
try {
|
||||
document.execCommand("formatBlock", false, isPTitle.value ? "P" : "H2");
|
||||
} catch (error) {
|
||||
console.error("应用段落格式失败:", error);
|
||||
}
|
||||
|
||||
|
||||
// 立即恢复滚动位置
|
||||
window.scrollTo(0, scrollTop);
|
||||
|
||||
|
||||
// 更新状态
|
||||
updatePTitleStatus();
|
||||
setTimeout(() => updatePTitleStatus(), 100);
|
||||
};
|
||||
|
||||
const updatePTitleStatus = () => {
|
||||
if (lastSelection) {
|
||||
const node = lastSelection.commonAncestorContainer;
|
||||
let parentElement = node.parentElement;
|
||||
|
||||
isPTitle.value = node.nodeName === "H2" || (node.nodeType === Node.TEXT_NODE && node.parentNode?.nodeName === "H2");
|
||||
|
||||
let parentElement = lastSelection.commonAncestorContainer;
|
||||
// 死循环,直到遇到终止条件
|
||||
while (true) {
|
||||
// 如果没有父元素了(到达文档根节点),退出循环返回false
|
||||
@@ -402,10 +412,11 @@ createApp({
|
||||
lastSelection.setEndAfter(textNode);
|
||||
|
||||
judgeIsEmpty();
|
||||
getCursorPosition();
|
||||
getCursorPosition("emoji");
|
||||
};
|
||||
|
||||
const getCursorPosition = () => {
|
||||
// 将当前输入位置滚动到可视区域
|
||||
const getCursorPosition = (type = "emoji") => {
|
||||
const range = lastSelection;
|
||||
const tempElement = document.createElement("span");
|
||||
tempElement.classList.add("cursor");
|
||||
@@ -420,13 +431,18 @@ createApp({
|
||||
|
||||
// 计算目标位置:中间偏上(视口高度的30%位置)
|
||||
// 公式:元素顶部相对于视口的位置 + 滚动距离 - 目标位置(视口高度的30%)
|
||||
const targetPosition = window.scrollY + rect.top - viewportHeight * 0.3; // 30%位置,比正中间更靠上
|
||||
// const targetPosition = window.scrollY + rect.top - viewportHeight * 0.3; // 30%位置,比正中间更靠上
|
||||
const height = type == "emoji" ? 300 : keyboardHeight;
|
||||
const targetPosition = window.scrollY + rect.top - (originalWindowHeight - height) + 40;
|
||||
console.log("originalWindowHeight", originalWindowHeight, "targetPosition", targetPosition, "window.scrollY", window.scrollY, "targetPosition");
|
||||
|
||||
// 平滑滚动到目标位置
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth", // 平滑滚动,移除则为瞬间滚动
|
||||
});
|
||||
if (Math.abs(targetPosition - window.scrollY) > 10) {
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth", // 平滑滚动,移除则为瞬间滚动
|
||||
});
|
||||
}
|
||||
|
||||
// 移除临时元素
|
||||
tempElement.parentNode.removeChild(tempElement);
|
||||
@@ -434,6 +450,25 @@ createApp({
|
||||
|
||||
const cutAnonymity = () => (info.value.anonymity = info.value.anonymity ? 0 : 1);
|
||||
|
||||
return { cutAnonymity, isEmpty, selectEmoji, closeEmoji, openEmoji, optionEmoji, emojiState, insertLabel, editorRef, info, title, titleLength, titleTextarea, adjustTextareaHeight, isPTitle, paragraphTitle, insertImage, onEditorInput };
|
||||
const operateRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时记录初始窗口高度
|
||||
originalWindowHeight = window.visualViewport.height;
|
||||
keyboardHeight = originalWindowHeight / 2;
|
||||
});
|
||||
|
||||
let originalWindowHeight = 0;
|
||||
let keyboardHeight = 0;
|
||||
|
||||
// 获取键盘高度
|
||||
const getKeyboardHeight = () => {
|
||||
const currentHeight = window.visualViewport.height;
|
||||
|
||||
// 键盘弹出时,窗口高度会减小
|
||||
if (currentHeight < originalWindowHeight) keyboardHeight = originalWindowHeight - currentHeight;
|
||||
};
|
||||
|
||||
return { operateRef, fixedState, onEditorBlur, onEditorFocus, cutAnonymity, isEmpty, selectEmoji, closeEmoji, openEmoji, optionEmoji, emojiState, insertLabel, editorRef, info, title, titleLength, titleTextarea, adjustTextareaHeight, isPTitle, paragraphTitle, insertImage, onEditorInput };
|
||||
},
|
||||
}).mount("#appIndex");
|
||||
|
||||
Reference in New Issue
Block a user