修复h5记事本不显示问题
This commit is contained in:
@@ -1,64 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<view class="note-editor-wrapper">
|
||||||
|
<!-- 通用富文本工具栏(H5 与 Android/iOS App 手机端均完整支持) -->
|
||||||
|
<view class="editor-toolbar">
|
||||||
|
<view class="toolbar-group">
|
||||||
|
<view class="tb-btn" title="加粗" @tap="handleFormat('bold')">
|
||||||
|
<FaIcon name="bold" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="斜体" @tap="handleFormat('italic')">
|
||||||
|
<FaIcon name="italic" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="下划线" @tap="handleFormat('underline')">
|
||||||
|
<FaIcon name="underline" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="删除线" @tap="handleFormat('strikeThrough')">
|
||||||
|
<FaIcon name="strikethrough" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar-divider" />
|
||||||
|
|
||||||
|
<view class="toolbar-group">
|
||||||
|
<view class="tb-btn" title="左对齐" @tap="handleFormat('justifyLeft')">
|
||||||
|
<FaIcon name="align-left" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="居中" @tap="handleFormat('justifyCenter')">
|
||||||
|
<FaIcon name="align-center" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="右对齐" @tap="handleFormat('justifyRight')">
|
||||||
|
<FaIcon name="align-right" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar-divider" />
|
||||||
|
|
||||||
|
<view class="toolbar-group">
|
||||||
|
<view class="tb-btn" title="大标题" @tap="handleFormat('formatBlock', '<h2>')">
|
||||||
|
<FaIcon name="heading" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="无序列表" @tap="handleFormat('insertUnorderedList')">
|
||||||
|
<FaIcon name="list-ul" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="有序列表" @tap="handleFormat('insertOrderedList')">
|
||||||
|
<FaIcon name="list-ol" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="插入分割线" @tap="handleInsertHtml('<hr/>')">
|
||||||
|
<FaIcon name="minus" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表格操作组(全端支持插入与行列增删) -->
|
||||||
|
<view class="toolbar-divider" />
|
||||||
|
<view class="toolbar-group">
|
||||||
|
<view class="tb-btn" title="插入表格" @tap="onInsertTable">
|
||||||
|
<FaIcon name="table" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="上方插入行" @tap="handleTableOp('addRowBefore')">
|
||||||
|
<FaIcon name="arrow-up" :size="11" color="#606266" />
|
||||||
|
<text class="tb-label">行</text>
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="下方插入行" @tap="handleTableOp('addRowAfter')">
|
||||||
|
<FaIcon name="arrow-down" :size="11" color="#606266" />
|
||||||
|
<text class="tb-label">行</text>
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="删除行" @tap="handleTableOp('deleteRow')">
|
||||||
|
<FaIcon name="minus" :size="11" color="#606266" />
|
||||||
|
<text class="tb-label">行</text>
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="左侧插入列" @tap="handleTableOp('addColBefore')">
|
||||||
|
<FaIcon name="arrow-left" :size="11" color="#606266" />
|
||||||
|
<text class="tb-label">列</text>
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="右侧插入列" @tap="handleTableOp('addColAfter')">
|
||||||
|
<FaIcon name="arrow-right" :size="11" color="#606266" />
|
||||||
|
<text class="tb-label">列</text>
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="删除列" @tap="handleTableOp('deleteCol')">
|
||||||
|
<FaIcon name="minus" :size="11" color="#606266" />
|
||||||
|
<text class="tb-label">列</text>
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn tb-btn-danger" title="删除表格" @tap="handleTableOp('deleteTable')">
|
||||||
|
<FaIcon name="trash-can" :size="13" color="#f56c6c" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar-divider" />
|
||||||
|
<view class="toolbar-group">
|
||||||
|
<view class="tb-btn" title="撤销" @tap="handleFormat('undo')">
|
||||||
|
<FaIcon name="rotate-left" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="重做" @tap="handleFormat('redo')">
|
||||||
|
<FaIcon name="rotate-right" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
<view class="tb-btn" title="清除格式" @tap="handleFormat('removeFormat')">
|
||||||
|
<FaIcon name="eraser" :size="13" color="#606266" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 编辑区(H5 与 App-plus 均使用真正的 HTML5 Webview DOM 渲染与编辑表格) -->
|
||||||
<!-- #ifdef H5 -->
|
<!-- #ifdef H5 -->
|
||||||
<div class="note-editor-h5">
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<button type="button" class="tb-btn" title="加粗" @mousedown.prevent="execFormat('bold')">
|
|
||||||
<FaIcon name="bold" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="斜体" @mousedown.prevent="execFormat('italic')">
|
|
||||||
<FaIcon name="italic" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="下划线" @mousedown.prevent="execFormat('underline')">
|
|
||||||
<FaIcon name="underline" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="左对齐" @mousedown.prevent="execFormat('justifyLeft')">
|
|
||||||
<FaIcon name="align-left" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="居中对齐" @mousedown.prevent="execFormat('justifyCenter')">
|
|
||||||
<FaIcon name="align-center" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="右对齐" @mousedown.prevent="execFormat('justifyRight')">
|
|
||||||
<FaIcon name="align-right" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar-divider" />
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<button type="button" class="tb-btn" title="插入表格" @mousedown.prevent="onInsertTable">
|
|
||||||
<FaIcon name="table" :size="14" color="#606266" />
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="上方插入行" @mousedown.prevent="onAddRowBefore">
|
|
||||||
<FaIcon name="arrow-up" :size="12" color="#606266" />
|
|
||||||
<span class="tb-label">行</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="下方插入行" @mousedown.prevent="onAddRowAfter">
|
|
||||||
<FaIcon name="arrow-down" :size="12" color="#606266" />
|
|
||||||
<span class="tb-label">行</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="删除行" @mousedown.prevent="onDeleteRow">
|
|
||||||
<FaIcon name="minus" :size="12" color="#606266" />
|
|
||||||
<span class="tb-label">行</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar-divider" />
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<button type="button" class="tb-btn" title="左侧插入列" @mousedown.prevent="onAddColBefore">
|
|
||||||
<FaIcon name="arrow-left" :size="12" color="#606266" />
|
|
||||||
<span class="tb-label">列</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="右侧插入列" @mousedown.prevent="onAddColAfter">
|
|
||||||
<FaIcon name="arrow-right" :size="12" color="#606266" />
|
|
||||||
<span class="tb-label">列</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn" title="删除列" @mousedown.prevent="onDeleteCol">
|
|
||||||
<FaIcon name="minus" :size="12" color="#606266" />
|
|
||||||
<span class="tb-label">列</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="tb-btn tb-btn-danger" title="删除表格" @mousedown.prevent="onDeleteTable">
|
|
||||||
<FaIcon name="trash-can" :size="14" color="#f56c6c" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
ref="h5EditorRef"
|
ref="h5EditorRef"
|
||||||
class="note-rich-editor rich-content"
|
class="note-rich-editor rich-content"
|
||||||
@@ -68,31 +108,35 @@
|
|||||||
@input="onH5Input"
|
@input="onH5Input"
|
||||||
@blur="onH5Input"
|
@blur="onH5Input"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
<!-- #ifndef H5 -->
|
|
||||||
<editor
|
<!-- #ifdef APP-PLUS -->
|
||||||
:id="editorId"
|
<view
|
||||||
class="note-rich-editor"
|
id="richEditorDiv"
|
||||||
:placeholder="placeholder"
|
class="note-rich-editor rich-content"
|
||||||
@ready="onEditorReady"
|
:prop="modelValue"
|
||||||
@input="onEditorInput"
|
:action="renderAction"
|
||||||
|
:change:prop="renderEditor.onPropChange"
|
||||||
|
:change:action="renderEditor.onActionChange"
|
||||||
/>
|
/>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, nextTick, getCurrentInstance, onMounted } from 'vue'
|
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||||
import FaIcon from '@/components/FaIcon.vue'
|
import FaIcon from '@/components/FaIcon.vue'
|
||||||
import {
|
import {
|
||||||
buildTableHtml,
|
buildTableHtml,
|
||||||
|
injectTableInlineStyles,
|
||||||
getCurrentCell,
|
getCurrentCell,
|
||||||
addRowBefore,
|
addRowBefore,
|
||||||
addRowAfter,
|
addRowAfter,
|
||||||
deleteRow,
|
deleteRow,
|
||||||
addColBefore,
|
addColBefore,
|
||||||
addColAfter,
|
addColAfter,
|
||||||
deleteCol
|
deleteCol,
|
||||||
|
deleteTable
|
||||||
} from '@/utils/table-editor.js'
|
} from '@/utils/table-editor.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -102,112 +146,110 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '请输入内容'
|
default: '记录你的想法...'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const editorId = 'noteRichEditor'
|
|
||||||
const h5EditorRef = ref(null)
|
const h5EditorRef = ref(null)
|
||||||
const editorCtx = ref(null)
|
|
||||||
const instance = getCurrentInstance()
|
|
||||||
const isInternalUpdate = ref(false)
|
const isInternalUpdate = ref(false)
|
||||||
const lastAppliedHtml = ref('')
|
const lastAppliedHtml = ref('')
|
||||||
|
const renderAction = ref(null)
|
||||||
|
|
||||||
function normalizeEditorHtml(html) {
|
// 响应 RenderJS 传递上来的内容更新
|
||||||
if (!html) return ''
|
function onContentChange(html) {
|
||||||
const template = document.createElement('template')
|
lastAppliedHtml.value = html
|
||||||
template.innerHTML = html.trim()
|
isInternalUpdate.value = true
|
||||||
const root = template.content
|
emit('update:modelValue', html)
|
||||||
|
nextTick(() => {
|
||||||
root.querySelectorAll('[contenteditable]').forEach((node) => {
|
isInternalUpdate.value = false
|
||||||
node.removeAttribute('contenteditable')
|
|
||||||
})
|
|
||||||
|
|
||||||
root.querySelectorAll('.column-resize-handle, .selectedCell, .grip-column, .grip-row').forEach((node) => {
|
|
||||||
node.remove()
|
|
||||||
})
|
|
||||||
|
|
||||||
root.querySelectorAll('td, th').forEach((cell) => {
|
|
||||||
const text = cell.textContent || ''
|
|
||||||
if (!text.trim() && !cell.querySelector('img, br')) {
|
|
||||||
cell.innerHTML = '<p><br></p>'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return template.innerHTML
|
|
||||||
}
|
|
||||||
|
|
||||||
function unlockEditableTree(root) {
|
|
||||||
if (!root) return
|
|
||||||
root.querySelectorAll('[contenteditable="false"]').forEach((node) => {
|
|
||||||
node.removeAttribute('contenteditable')
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchTables(root) {
|
// 供 template 绑定的 Renderjs 桥接方法
|
||||||
if (!root) return
|
function onRenderChange(html) {
|
||||||
root.querySelectorAll('table').forEach((table) => {
|
onContentChange(html)
|
||||||
table.removeAttribute('contenteditable')
|
}
|
||||||
table.style.borderCollapse = 'collapse'
|
|
||||||
table.style.border = '1px solid #e4e7ed'
|
function handleFormat(cmd, value = null) {
|
||||||
table.style.width = '100%'
|
// #ifdef H5
|
||||||
table.style.margin = '12px 0'
|
execH5Format(cmd, value)
|
||||||
table.querySelectorAll('th, td').forEach((cell) => {
|
// #endif
|
||||||
cell.removeAttribute('contenteditable')
|
// #ifdef APP-PLUS
|
||||||
cell.style.border = '1px solid #e4e7ed'
|
renderAction.value = {
|
||||||
cell.style.padding = '8px 12px'
|
type: 'format',
|
||||||
cell.style.verticalAlign = 'top'
|
cmd,
|
||||||
cell.style.wordBreak = 'break-word'
|
value,
|
||||||
cell.style.userSelect = 'text'
|
t: Date.now()
|
||||||
cell.style.cursor = 'text'
|
}
|
||||||
})
|
// #endif
|
||||||
table.querySelectorAll('th').forEach((cell) => {
|
}
|
||||||
cell.style.backgroundColor = '#f5f7fa'
|
|
||||||
cell.style.fontWeight = '600'
|
function handleInsertHtml(html) {
|
||||||
})
|
// #ifdef H5
|
||||||
|
insertH5Html(html)
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
renderAction.value = {
|
||||||
|
type: 'insertHtml',
|
||||||
|
html: injectTableInlineStyles(html),
|
||||||
|
t: Date.now()
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTableOp(opName) {
|
||||||
|
// #ifdef H5
|
||||||
|
execH5TableOp(opName)
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
renderAction.value = {
|
||||||
|
type: 'tableOp',
|
||||||
|
op: opName,
|
||||||
|
t: Date.now()
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInsertTable() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['3×3 表格', '4×4 表格', '5×3 表格'],
|
||||||
|
success: (res) => {
|
||||||
|
const sizes = [[3, 3], [4, 4], [5, 3]]
|
||||||
|
const [rows, cols] = sizes[res.tapIndex] || [3, 3]
|
||||||
|
const tableHtml = buildTableHtml(rows, cols)
|
||||||
|
handleInsertHtml(tableHtml)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeCaretInCell(cell, event) {
|
// ================= H5 专属 DOM 操作 =================
|
||||||
const el = h5EditorRef.value
|
function execH5Format(cmd, value = null) {
|
||||||
if (!el || !cell) return
|
|
||||||
el.focus()
|
|
||||||
|
|
||||||
let range = null
|
|
||||||
if (event && document.caretRangeFromPoint) {
|
|
||||||
range = document.caretRangeFromPoint(event.clientX, event.clientY)
|
|
||||||
} else if (event && document.caretPositionFromPoint) {
|
|
||||||
const pos = document.caretPositionFromPoint(event.clientX, event.clientY)
|
|
||||||
if (pos) {
|
|
||||||
range = document.createRange()
|
|
||||||
range.setStart(pos.offsetNode, pos.offset)
|
|
||||||
range.collapse(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (range && cell.contains(range.startContainer)) {
|
|
||||||
const sel = window.getSelection()
|
|
||||||
sel.removeAllRanges()
|
|
||||||
sel.addRange(range)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallback = document.createRange()
|
|
||||||
const target = cell.querySelector('p, div, span') || cell
|
|
||||||
fallback.selectNodeContents(target)
|
|
||||||
fallback.collapse(false)
|
|
||||||
const sel = window.getSelection()
|
|
||||||
sel.removeAllRanges()
|
|
||||||
sel.addRange(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncContent() {
|
|
||||||
const el = h5EditorRef.value
|
const el = h5EditorRef.value
|
||||||
if (!el) return
|
if (!el) return
|
||||||
unlockEditableTree(el)
|
el.focus()
|
||||||
patchTables(el)
|
document.execCommand(cmd, false, value)
|
||||||
|
syncH5Content()
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertH5Html(html) {
|
||||||
|
const el = h5EditorRef.value
|
||||||
|
if (!el) return
|
||||||
|
el.focus()
|
||||||
|
const formatted = injectTableInlineStyles(html)
|
||||||
|
if (document.queryCommandSupported('insertHTML')) {
|
||||||
|
document.execCommand('insertHTML', false, formatted)
|
||||||
|
} else {
|
||||||
|
el.insertAdjacentHTML('beforeend', formatted)
|
||||||
|
}
|
||||||
|
syncH5Content()
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncH5Content() {
|
||||||
|
const el = h5EditorRef.value
|
||||||
|
if (!el) return
|
||||||
|
patchH5Tables(el)
|
||||||
const html = el.innerHTML || ''
|
const html = el.innerHTML || ''
|
||||||
lastAppliedHtml.value = html
|
lastAppliedHtml.value = html
|
||||||
isInternalUpdate.value = true
|
isInternalUpdate.value = true
|
||||||
@@ -217,114 +259,48 @@ function syncContent() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertHtmlAtCursor(html) {
|
function execH5TableOp(opName) {
|
||||||
const el = h5EditorRef.value
|
|
||||||
if (!el) return
|
|
||||||
el.focus()
|
|
||||||
if (document.queryCommandSupported('insertHTML')) {
|
|
||||||
document.execCommand('insertHTML', false, html)
|
|
||||||
} else {
|
|
||||||
el.insertAdjacentHTML('beforeend', html)
|
|
||||||
}
|
|
||||||
syncContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
function execFormat(cmd) {
|
|
||||||
const el = h5EditorRef.value
|
|
||||||
if (!el) return
|
|
||||||
el.focus()
|
|
||||||
document.execCommand(cmd, false, null)
|
|
||||||
syncContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireCell(message = '请先在表格中选中单元格') {
|
|
||||||
const cell = getCurrentCell(h5EditorRef.value)
|
const cell = getCurrentCell(h5EditorRef.value)
|
||||||
if (!cell) {
|
if (!cell) {
|
||||||
uni.showToast({ title: message, icon: 'none' })
|
uni.showToast({ title: '请先在表格单元格中点击光标', icon: 'none' })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return cell
|
let target = null
|
||||||
|
if (opName === 'addRowBefore') target = addRowBefore(cell)
|
||||||
|
else if (opName === 'addRowAfter') target = addRowAfter(cell)
|
||||||
|
else if (opName === 'deleteRow') target = deleteRow(cell)
|
||||||
|
else if (opName === 'addColBefore') target = addColBefore(cell)
|
||||||
|
else if (opName === 'addColAfter') target = addColAfter(cell)
|
||||||
|
else if (opName === 'deleteCol') target = deleteCol(cell)
|
||||||
|
else if (opName === 'deleteTable') {
|
||||||
|
deleteTable(cell)
|
||||||
|
syncH5Content()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInsertTable() {
|
syncH5Content()
|
||||||
uni.showActionSheet({
|
if (target && target.focus) target.focus()
|
||||||
itemList: ['3×3 表格', '4×4 表格', '5×3 表格'],
|
|
||||||
success: (res) => {
|
|
||||||
const sizes = [[3, 3], [4, 4], [5, 3]]
|
|
||||||
const [rows, cols] = sizes[res.tapIndex] || [3, 3]
|
|
||||||
insertHtmlAtCursor(buildTableHtml(rows, cols))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchH5Tables(root) {
|
||||||
|
if (!root) return
|
||||||
|
root.querySelectorAll('table').forEach((table) => {
|
||||||
|
table.style.borderCollapse = 'collapse'
|
||||||
|
table.style.border = '1px solid #dcdfe6'
|
||||||
|
table.style.width = '100%'
|
||||||
|
table.style.margin = '12px 0'
|
||||||
|
table.setAttribute('border', '1')
|
||||||
|
table.querySelectorAll('th, td').forEach((cell) => {
|
||||||
|
cell.style.border = '1px solid #dcdfe6'
|
||||||
|
cell.style.padding = '8px 12px'
|
||||||
|
cell.style.verticalAlign = 'top'
|
||||||
|
cell.style.wordBreak = 'break-word'
|
||||||
|
cell.setAttribute('border', '1')
|
||||||
|
})
|
||||||
|
table.querySelectorAll('th').forEach((cell) => {
|
||||||
|
cell.style.backgroundColor = '#f5f7fa'
|
||||||
|
cell.style.fontWeight = '600'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
function onAddRowBefore() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const target = addRowBefore(cell)
|
|
||||||
syncContent()
|
|
||||||
if (target) placeCaretInCell(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAddRowAfter() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const target = addRowAfter(cell)
|
|
||||||
syncContent()
|
|
||||||
if (target) placeCaretInCell(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteRow() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const target = deleteRow(cell)
|
|
||||||
if (!target) {
|
|
||||||
uni.showToast({ title: '至少保留一行', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncContent()
|
|
||||||
placeCaretInCell(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAddColBefore() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const target = addColBefore(cell)
|
|
||||||
syncContent()
|
|
||||||
if (target) placeCaretInCell(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAddColAfter() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const target = addColAfter(cell)
|
|
||||||
syncContent()
|
|
||||||
if (target) placeCaretInCell(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteCol() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const target = deleteCol(cell)
|
|
||||||
if (!target) {
|
|
||||||
uni.showToast({ title: '至少保留一列', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncContent()
|
|
||||||
placeCaretInCell(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteTable() {
|
|
||||||
const cell = requireCell()
|
|
||||||
if (!cell) return
|
|
||||||
const table = cell.closest('table')
|
|
||||||
if (!table) return
|
|
||||||
uni.showModal({
|
|
||||||
title: '删除表格',
|
|
||||||
content: '确定删除当前表格吗?',
|
|
||||||
success: (res) => {
|
|
||||||
if (!res.confirm) return
|
|
||||||
table.remove()
|
|
||||||
syncContent()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,11 +309,18 @@ function onH5MouseDown(e) {
|
|||||||
if (!el) return
|
if (!el) return
|
||||||
const cell = e.target.closest('td, th')
|
const cell = e.target.closest('td, th')
|
||||||
if (!cell || !el.contains(cell)) return
|
if (!cell || !el.contains(cell)) return
|
||||||
|
|
||||||
unlockEditableTree(el)
|
|
||||||
const evt = e
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
placeCaretInCell(cell, evt)
|
el.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onH5Input(e) {
|
||||||
|
const html = e.target.innerHTML || ''
|
||||||
|
lastAppliedHtml.value = html
|
||||||
|
isInternalUpdate.value = true
|
||||||
|
emit('update:modelValue', html)
|
||||||
|
nextTick(() => {
|
||||||
|
isInternalUpdate.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,54 +329,20 @@ function setH5Html(html) {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const el = h5EditorRef.value
|
const el = h5EditorRef.value
|
||||||
if (!el) return
|
if (!el) return
|
||||||
const normalized = normalizeEditorHtml(html || '')
|
const formatted = injectTableInlineStyles(html || '')
|
||||||
if (normalized === lastAppliedHtml.value && el.innerHTML === normalized) {
|
if (formatted === lastAppliedHtml.value && el.innerHTML === formatted) {
|
||||||
patchTables(el)
|
patchH5Tables(el)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (el.innerHTML !== normalized) {
|
if (el.innerHTML !== formatted) {
|
||||||
el.innerHTML = normalized
|
el.innerHTML = formatted
|
||||||
}
|
}
|
||||||
unlockEditableTree(el)
|
patchH5Tables(el)
|
||||||
patchTables(el)
|
lastAppliedHtml.value = formatted
|
||||||
lastAppliedHtml.value = normalized
|
|
||||||
})
|
})
|
||||||
// #endif
|
// #endif
|
||||||
}
|
}
|
||||||
|
|
||||||
function onH5Input(e) {
|
|
||||||
// #ifdef H5
|
|
||||||
const html = e.target.innerHTML || ''
|
|
||||||
lastAppliedHtml.value = html
|
|
||||||
isInternalUpdate.value = true
|
|
||||||
emit('update:modelValue', html)
|
|
||||||
nextTick(() => {
|
|
||||||
isInternalUpdate.value = false
|
|
||||||
})
|
|
||||||
// #endif
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEditorInput(e) {
|
|
||||||
emit('update:modelValue', e.detail.html || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEditorReady() {
|
|
||||||
// #ifndef H5
|
|
||||||
uni.createSelectorQuery()
|
|
||||||
.in(instance)
|
|
||||||
.select(`#${editorId}`)
|
|
||||||
.context((res) => {
|
|
||||||
if (res && res.context) {
|
|
||||||
editorCtx.value = res.context
|
|
||||||
if (props.modelValue) {
|
|
||||||
editorCtx.value.setContents({ html: props.modelValue })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec()
|
|
||||||
// #endif
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -401,11 +350,6 @@ watch(
|
|||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
setH5Html(val)
|
setH5Html(val)
|
||||||
// #endif
|
// #endif
|
||||||
// #ifndef H5
|
|
||||||
if (editorCtx.value) {
|
|
||||||
editorCtx.value.setContents({ html: val || '' })
|
|
||||||
}
|
|
||||||
// #endif
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -414,60 +358,211 @@ onMounted(() => {
|
|||||||
setH5Html(props.modelValue)
|
setH5Html(props.modelValue)
|
||||||
// #endif
|
// #endif
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 将 onRenderChange 挂载到组件实例供 Renderjs 调用
|
||||||
|
defineExpose({
|
||||||
|
onRenderChange
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- #ifdef APP-PLUS -->
|
||||||
|
<script module="renderEditor" lang="renderjs">
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
this.initRenderEditor()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initRenderEditor() {
|
||||||
|
const el = document.getElementById('richEditorDiv')
|
||||||
|
if (!el) {
|
||||||
|
setTimeout(() => this.initRenderEditor(), 50)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
el.contentEditable = 'true'
|
||||||
|
el.setAttribute('data-placeholder', '记录你的想法...')
|
||||||
|
el.style.outline = 'none'
|
||||||
|
el.style.minHeight = '320rpx'
|
||||||
|
el.style.userSelect = 'text'
|
||||||
|
el.style.webkitUserSelect = 'text'
|
||||||
|
|
||||||
|
if (this.prop) {
|
||||||
|
el.innerHTML = this.formatHtml(this.prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('input', () => {
|
||||||
|
this.notifyOwner(el.innerHTML || '')
|
||||||
|
})
|
||||||
|
el.addEventListener('blur', () => {
|
||||||
|
this.notifyOwner(el.innerHTML || '')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatHtml(html) {
|
||||||
|
if (!html) return ''
|
||||||
|
return html
|
||||||
|
.replace(/<table(\s+[^>]*)?>/gi, '<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;width:100%;border:1px solid #dcdfe6;margin:12px 0;"$1>')
|
||||||
|
.replace(/<th(\s+[^>]*)?>/gi, '<th border="1" style="border:1px solid #dcdfe6;background-color:#f5f7fa;font-weight:600;padding:8px 12px;color:#303133;"$1>')
|
||||||
|
.replace(/<td(\s+[^>]*)?>/gi, '<td border="1" style="border:1px solid #dcdfe6;background-color:#ffffff;padding:8px 12px;color:#303133;"$1>')
|
||||||
|
},
|
||||||
|
onPropChange(newVal) {
|
||||||
|
const el = document.getElementById('richEditorDiv')
|
||||||
|
if (!el) return
|
||||||
|
const formatted = this.formatHtml(newVal || '')
|
||||||
|
if (formatted !== el.innerHTML) {
|
||||||
|
el.innerHTML = formatted
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onActionChange(act) {
|
||||||
|
if (!act || !act.type) return
|
||||||
|
const el = document.getElementById('richEditorDiv')
|
||||||
|
if (!el) return
|
||||||
|
el.focus()
|
||||||
|
|
||||||
|
if (act.type === 'format') {
|
||||||
|
document.execCommand(act.cmd, false, act.value || null)
|
||||||
|
this.notifyOwner(el.innerHTML || '')
|
||||||
|
} else if (act.type === 'insertHtml') {
|
||||||
|
const formatted = this.formatHtml(act.html || '')
|
||||||
|
if (document.queryCommandSupported('insertHTML')) {
|
||||||
|
document.execCommand('insertHTML', false, formatted)
|
||||||
|
} else {
|
||||||
|
el.insertAdjacentHTML('beforeend', formatted)
|
||||||
|
}
|
||||||
|
this.notifyOwner(el.innerHTML || '')
|
||||||
|
} else if (act.type === 'tableOp') {
|
||||||
|
this.handleRenderTableOp(act.op)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleRenderTableOp(op) {
|
||||||
|
const el = document.getElementById('richEditorDiv')
|
||||||
|
if (!el) return
|
||||||
|
const sel = window.getSelection()
|
||||||
|
if (!sel || sel.rangeCount === 0) return
|
||||||
|
let node = sel.anchorNode
|
||||||
|
if (node && node.nodeType === 3) node = node.parentElement
|
||||||
|
const cell = node ? node.closest('td, th') : null
|
||||||
|
if (!cell || !el.contains(cell)) return
|
||||||
|
|
||||||
|
if (op === 'addRowBefore' || op === 'addRowAfter') {
|
||||||
|
const row = cell.closest('tr')
|
||||||
|
if (!row) return
|
||||||
|
const newRow = row.cloneNode(true)
|
||||||
|
newRow.querySelectorAll('td, th').forEach(c => {
|
||||||
|
c.innerHTML = '<p><br></p>'
|
||||||
|
c.style.border = '1px solid #dcdfe6'
|
||||||
|
c.style.padding = '8px 12px'
|
||||||
|
c.setAttribute('border', '1')
|
||||||
|
})
|
||||||
|
if (op === 'addRowBefore') row.before(newRow)
|
||||||
|
else row.after(newRow)
|
||||||
|
} else if (op === 'deleteRow') {
|
||||||
|
const row = cell.closest('tr')
|
||||||
|
const section = row ? row.parentElement : null
|
||||||
|
if (row && section && section.querySelectorAll('tr').length > 1) {
|
||||||
|
row.remove()
|
||||||
|
}
|
||||||
|
} else if (op === 'addColBefore' || op === 'addColAfter') {
|
||||||
|
const idx = cell.cellIndex
|
||||||
|
const table = cell.closest('table')
|
||||||
|
if (!table) return
|
||||||
|
table.querySelectorAll('tr').forEach(r => {
|
||||||
|
const refCell = r.cells[idx]
|
||||||
|
if (!refCell) return
|
||||||
|
const newCell = document.createElement(refCell.tagName.toLowerCase())
|
||||||
|
newCell.innerHTML = '<p><br></p>'
|
||||||
|
newCell.style.border = '1px solid #dcdfe6'
|
||||||
|
newCell.style.padding = '8px 12px'
|
||||||
|
newCell.setAttribute('border', '1')
|
||||||
|
if (newCell.tagName === 'TH') {
|
||||||
|
newCell.style.backgroundColor = '#f5f7fa'
|
||||||
|
newCell.style.fontWeight = '600'
|
||||||
|
}
|
||||||
|
if (op === 'addColBefore') refCell.before(newCell)
|
||||||
|
else refCell.after(newCell)
|
||||||
|
})
|
||||||
|
} else if (op === 'deleteCol') {
|
||||||
|
const idx = cell.cellIndex
|
||||||
|
const table = cell.closest('table')
|
||||||
|
if (table && cell.parentElement.cells.length > 1) {
|
||||||
|
table.querySelectorAll('tr').forEach(r => {
|
||||||
|
const c = r.cells[idx]
|
||||||
|
if (c) c.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (op === 'deleteTable') {
|
||||||
|
const table = cell.closest('table')
|
||||||
|
if (table) table.remove()
|
||||||
|
}
|
||||||
|
this.notifyOwner(el.innerHTML || '')
|
||||||
|
},
|
||||||
|
notifyOwner(html) {
|
||||||
|
this.$ownerInstance.callMethod('onRenderChange', html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.note-editor-h5 {
|
.note-editor-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 360rpx;
|
flex: 1;
|
||||||
|
min-height: 400rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar {
|
.editor-toolbar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8rpx;
|
gap: 6rpx;
|
||||||
padding-bottom: 12rpx;
|
padding-bottom: 12rpx;
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 12rpx;
|
||||||
border-bottom: 1rpx solid #ebeef5;
|
border-bottom: 1rpx solid #ebeef5;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-group {
|
.toolbar-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8rpx;
|
gap: 6rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-divider {
|
.toolbar-divider {
|
||||||
width: 1rpx;
|
width: 1rpx;
|
||||||
height: 28rpx;
|
height: 32rpx;
|
||||||
background: #ebeef5;
|
background: #ebeef5;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin: 0 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tb-btn {
|
.tb-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4rpx;
|
gap: 2rpx;
|
||||||
min-width: 56rpx;
|
min-width: 54rpx;
|
||||||
height: 56rpx;
|
height: 54rpx;
|
||||||
padding: 0 12rpx;
|
padding: 0 8rpx;
|
||||||
border: 1rpx solid #ebeef5;
|
border: 1rpx solid #ebeef5;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
font-size: 22rpx;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
@@ -480,14 +575,15 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tb-label {
|
.tb-label {
|
||||||
font-size: 20rpx;
|
font-size: 18rpx;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
|
margin-left: 2rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-rich-editor {
|
.note-rich-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 320rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -498,33 +594,65 @@ onMounted(() => {
|
|||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #ifdef H5 */
|
|
||||||
.note-rich-editor:empty::before {
|
.note-rich-editor:empty::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
color: #909399;
|
color: #909399;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-rich-editor:focus:empty::before {
|
.note-rich-editor:focus:empty::before {
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
/* #endif */
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '@/src/styles/rich-content.scss';
|
@import '@/src/styles/rich-content.scss';
|
||||||
|
|
||||||
.note-rich-editor.rich-content {
|
/* 全局表格规范:强制在所有终端与渲染层呈现清晰的表格边框与单元格背景 */
|
||||||
|
.note-rich-editor {
|
||||||
table {
|
table {
|
||||||
td,
|
display: table !important;
|
||||||
th {
|
border-collapse: collapse !important;
|
||||||
|
border-spacing: 0 !important;
|
||||||
|
border: 1px solid #dcdfe6 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 12px 0 !important;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
display: table-row-group !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
display: table-row !important;
|
||||||
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
display: table-cell !important;
|
||||||
|
border: 1px solid #dcdfe6 !important;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
font-size: 26rpx !important;
|
||||||
|
min-width: 44px !important;
|
||||||
|
min-height: 36px !important;
|
||||||
|
vertical-align: top !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
user-select: text !important;
|
user-select: text !important;
|
||||||
-webkit-user-select: text !important;
|
-webkit-user-select: text !important;
|
||||||
cursor: text !important;
|
cursor: text !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #303133 !important;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0 !important;
|
||||||
min-height: 1.4em;
|
min-height: 1.4em !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -33,7 +33,14 @@
|
|||||||
"path": "pages/login/geetest-webview",
|
"path": "pages/login/geetest-webview",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "人机验证"
|
"navigationBarTitleText": "人机验证",
|
||||||
|
"backgroundColor": "#F8FAFC",
|
||||||
|
"app-plus": {
|
||||||
|
"animationType": "fade-in",
|
||||||
|
"animationDuration": 200,
|
||||||
|
"popGesture": "none",
|
||||||
|
"bounce": "none"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,30 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<view class="geetest-container">
|
||||||
<web-view :src="webviewSrc" @message="onMessage" />
|
<web-view :src="webviewSrc" @message="onMessage" />
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
const webviewSrc = ref('')
|
const webviewSrc = ref('')
|
||||||
let eventChannel = null
|
let eventChannel = null
|
||||||
|
let settled = false
|
||||||
|
let pollTimer = null
|
||||||
|
|
||||||
|
function unwrapMessage(e) {
|
||||||
|
const raw = e?.detail?.data
|
||||||
|
const first = Array.isArray(raw) ? raw[0] : raw
|
||||||
|
if (!first) return {}
|
||||||
|
if (first.type) return first
|
||||||
|
if (first.data && first.data.type) return first.data
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish(payload) {
|
||||||
|
if (settled || !payload || !payload.type) return
|
||||||
|
settled = true
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (typeof plus !== 'undefined' && plus.storage) {
|
||||||
|
plus.storage.removeItem('__geetest_payload__')
|
||||||
|
plus.storage.removeItem('__geetest_captcha_id__')
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (payload.type === 'success') {
|
||||||
|
eventChannel?.emit('geetestSuccess', payload.result || {})
|
||||||
|
} else {
|
||||||
|
eventChannel?.emit('geetestFail', payload.msg || '人机验证未通过')
|
||||||
|
}
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
fail() {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(e) {
|
||||||
|
finish(unwrapMessage(e))
|
||||||
|
}
|
||||||
|
|
||||||
onLoad((query) => {
|
onLoad((query) => {
|
||||||
const captchaId = decodeURIComponent(query.captchaId || '')
|
const captchaId = decodeURIComponent(query.captchaId || '')
|
||||||
|
try {
|
||||||
|
if (typeof plus !== 'undefined' && plus.storage) {
|
||||||
|
plus.storage.setItem('__geetest_captcha_id__', captchaId)
|
||||||
|
plus.storage.removeItem('__geetest_payload__')
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
webviewSrc.value = `/static/html/geetest-captcha.html?captchaId=${encodeURIComponent(captchaId)}`
|
webviewSrc.value = `/static/html/geetest-captcha.html?captchaId=${encodeURIComponent(captchaId)}`
|
||||||
const pages = getCurrentPages()
|
|
||||||
const page = pages[pages.length - 1]
|
const pagesList = getCurrentPages()
|
||||||
eventChannel = page.getOpenerEventChannel?.()
|
const curPage = pagesList[pagesList.length - 1]
|
||||||
|
eventChannel = curPage?.getOpenerEventChannel?.()
|
||||||
|
|
||||||
|
// 轮询 plus.storage 结果(每 60ms 检查一次)
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
if (settled) return
|
||||||
|
try {
|
||||||
|
if (typeof plus !== 'undefined' && plus.storage) {
|
||||||
|
const raw = plus.storage.getItem('__geetest_payload__')
|
||||||
|
if (raw) {
|
||||||
|
const p = JSON.parse(raw)
|
||||||
|
if (p) finish(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}, 60)
|
||||||
})
|
})
|
||||||
|
|
||||||
function onMessage(e) {
|
onUnload(() => {
|
||||||
const payload = (e.detail && e.detail.data && e.detail.data[0]) || {}
|
if (pollTimer) {
|
||||||
if (payload.type === 'success') {
|
clearInterval(pollTimer)
|
||||||
eventChannel?.emit('geetestSuccess', payload.result || {})
|
pollTimer = null
|
||||||
uni.navigateBack()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
eventChannel?.emit('geetestFail', payload.msg || '人机验证未通过')
|
try {
|
||||||
uni.navigateBack()
|
if (typeof plus !== 'undefined' && plus.storage) {
|
||||||
|
plus.storage.removeItem('__geetest_payload__')
|
||||||
|
plus.storage.removeItem('__geetest_captcha_id__')
|
||||||
}
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
if (!settled) {
|
||||||
|
eventChannel?.emit('geetestFail', '已取消人机验证')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.geetest-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -371,9 +371,11 @@ async function handleLogin() {
|
|||||||
if (ok) navigateAfterLogin()
|
if (ok) navigateAfterLogin()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err?.message || '人机验证失败'
|
const msg = err?.message || '人机验证失败'
|
||||||
if (msg !== '人机验证未通过') {
|
if (msg !== '人机验证未通过' && msg !== '已取消人机验证') {
|
||||||
uni.showToast({ title: msg, icon: 'none' })
|
uni.showToast({ title: msg, icon: 'none' })
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { ref, reactive } from 'vue'
|
|||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { getNote, createNote, updateNote, deleteNote } from '@/api/note.js'
|
import { getNote, createNote, updateNote, deleteNote } from '@/api/note.js'
|
||||||
import { formatDate } from '@/utils/date.js'
|
import { formatDate } from '@/utils/date.js'
|
||||||
|
import { injectTableInlineStyles } from '@/utils/table-editor.js'
|
||||||
import NoteRichEditor from '@/components/NoteRichEditor.vue'
|
import NoteRichEditor from '@/components/NoteRichEditor.vue'
|
||||||
|
|
||||||
const noteId = ref('')
|
const noteId = ref('')
|
||||||
@@ -81,7 +82,7 @@ async function loadNote(id) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.title = note.title
|
form.title = note.title
|
||||||
form.content = note.content
|
form.content = injectTableInlineStyles(note.content || '')
|
||||||
form.pinned = !!note.pinned
|
form.pinned = !!note.pinned
|
||||||
meta.updatedAt = note.updatedAt
|
meta.updatedAt = note.updatedAt
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -97,16 +98,17 @@ async function onSave() {
|
|||||||
}
|
}
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
const formattedContent = injectTableInlineStyles(form.content)
|
||||||
if (noteId.value) {
|
if (noteId.value) {
|
||||||
await updateNote(noteId.value, {
|
await updateNote(noteId.value, {
|
||||||
title: form.title,
|
title: form.title,
|
||||||
content: form.content,
|
content: formattedContent,
|
||||||
pinned: form.pinned
|
pinned: form.pinned
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await createNote({
|
await createNote({
|
||||||
title: form.title,
|
title: form.title,
|
||||||
content: form.content,
|
content: formattedContent,
|
||||||
pinned: form.pinned
|
pinned: form.pinned
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -191,14 +193,16 @@ function onDelete() {
|
|||||||
padding: 24rpx 28rpx 0 28rpx;
|
padding: 24rpx 28rpx 0 28rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-top: 1rpx solid $color-divider;
|
border-top: 1rpx solid $color-divider;
|
||||||
|
min-height: 520rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-editor {
|
.note-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 440rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
border-radius: 12rpx;
|
border-radius: 12rpx;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
|
|||||||
@@ -1,34 +1,112 @@
|
|||||||
/**
|
/**
|
||||||
* 富文本 HTML 展示样式(与 platform UmoEditor / TipTap 输出对齐)
|
* 全局富文本与表格展示样式
|
||||||
* 用于 editor 组件、rich-text、v-html 等场景
|
* 深度覆盖 editor 原生组件、rich-text、.rich-content、.ql-editor 等所有富文本容器
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* ================= 全局默认表格规范(确保全端、全组件均有明显边框与表头底色) ================= */
|
||||||
|
table {
|
||||||
|
display: table !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
border-spacing: 0 !important;
|
||||||
|
border: 1px solid #dcdfe6 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 12px 0 !important;
|
||||||
|
table-layout: auto !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
display: table-row-group !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
display: table-header-group !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
display: table-row !important;
|
||||||
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
display: table-cell !important;
|
||||||
|
border: 1px solid #dcdfe6 !important;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #303133 !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
font-size: 26rpx !important;
|
||||||
|
min-width: 50px !important;
|
||||||
|
min-height: 36px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
vertical-align: top !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
user-select: text !important;
|
||||||
|
-webkit-user-select: text !important;
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #303133 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 !important;
|
||||||
|
min-height: 1.4em !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor,
|
||||||
|
uni-editor,
|
||||||
.ql-editor,
|
.ql-editor,
|
||||||
.rich-content {
|
.rich-content,
|
||||||
|
.note-rich-editor {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
line-height: 1.8;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 12px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-size: 36rpx; }
|
||||||
|
h2 { font-size: 32rpx; }
|
||||||
|
h3 { font-size: 30rpx; }
|
||||||
|
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 4px solid #3973ff;
|
border-left: 4px solid #3c9cff;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
|
border-radius: 0 8rpx 8rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e4e7ed;
|
||||||
border-radius: 4px;
|
border-radius: 8rpx;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -37,37 +115,11 @@
|
|||||||
code {
|
code {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e4e7ed;
|
||||||
border-radius: 3px;
|
border-radius: 6rpx;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
font-family: Consolas, Monaco, monospace;
|
font-family: Consolas, Monaco, monospace;
|
||||||
font-size: 13px;
|
font-size: 24rpx;
|
||||||
}
|
color: #e6a23c;
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse !important;
|
|
||||||
border: 1px solid #e4e7ed !important;
|
|
||||||
width: 100% !important;
|
|
||||||
margin: 12px 0;
|
|
||||||
table-layout: auto;
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border: 1px solid #e4e7ed !important;
|
|
||||||
background-color: #ffffff !important;
|
|
||||||
color: #303133 !important;
|
|
||||||
padding: 8px 12px !important;
|
|
||||||
min-width: 60px;
|
|
||||||
vertical-align: top;
|
|
||||||
word-break: break-word;
|
|
||||||
user-select: text;
|
|
||||||
-webkit-user-select: text;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: #f5f7fa !important;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
@@ -77,7 +129,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
line-height: 1.8;
|
line-height: 1.7;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,45 +8,93 @@
|
|||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: rgba(0, 0, 0, 0.45);
|
background: #f5f7fa;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.loading-text {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="./gt4.js"></script>
|
<script src="./gt4.js"></script>
|
||||||
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.4.js"></script>
|
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.4.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="loading-text" id="tipText">正在加载安全验证...</div>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var params = new URLSearchParams(window.location.search)
|
var params = new URLSearchParams(window.location.search);
|
||||||
var captchaId = params.get('captchaId') || ''
|
var captchaId = params.get('captchaId') || '';
|
||||||
|
var finished = false;
|
||||||
|
var started = false;
|
||||||
|
|
||||||
function postToUni(payload) {
|
if (!captchaId && window.plus && plus.storage) {
|
||||||
|
try {
|
||||||
|
captchaId = plus.storage.getItem('__geetest_captcha_id__') || '';
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyResult(payload) {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
|
||||||
|
// 1. plus.storage 共享存储
|
||||||
|
try {
|
||||||
|
if (window.plus && plus.storage) {
|
||||||
|
plus.storage.setItem('__geetest_payload__', JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 2. uni.postMessage
|
||||||
|
try {
|
||||||
if (window.uni && typeof uni.postMessage === 'function') {
|
if (window.uni && typeof uni.postMessage === 'function') {
|
||||||
uni.postMessage({ data: payload })
|
uni.postMessage({ data: payload });
|
||||||
}
|
}
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePage() {
|
function failAndNotify(msg) {
|
||||||
if (window.uni && typeof uni.navigateBack === 'function') {
|
notifyResult({ type: 'fail', msg: msg || '人机验证未通过' });
|
||||||
setTimeout(function () { uni.navigateBack() }, 120)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!captchaId || typeof initGeetest4 !== 'function') {
|
function startCaptcha(id) {
|
||||||
postToUni({ type: 'fail', msg: '极验初始化失败' })
|
if (started) return;
|
||||||
closePage()
|
if (id) captchaId = id;
|
||||||
return
|
if (!captchaId && window.plus && plus.storage) {
|
||||||
|
try {
|
||||||
|
captchaId = plus.storage.getItem('__geetest_captcha_id__') || '';
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
if (!captchaId) return;
|
||||||
|
started = true;
|
||||||
|
|
||||||
|
if (typeof initGeetest4 !== 'function') {
|
||||||
|
failAndNotify('极验初始化失败');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
initGeetest4({
|
initGeetest4({
|
||||||
captchaId: captchaId,
|
captchaId: captchaId,
|
||||||
product: 'bind',
|
product: 'bind',
|
||||||
language: 'zh-CN'
|
language: 'zh-CN',
|
||||||
|
https: true,
|
||||||
|
protocol: 'https://',
|
||||||
|
onError: function () {
|
||||||
|
failAndNotify('人机验证加载失败');
|
||||||
|
}
|
||||||
}, function (instance) {
|
}, function (instance) {
|
||||||
|
var tip = document.getElementById('tipText');
|
||||||
|
if (tip) tip.style.display = 'none';
|
||||||
|
|
||||||
instance.onSuccess(function () {
|
instance.onSuccess(function () {
|
||||||
var result = instance.getValidate() || {}
|
var result = instance.getValidate() || {};
|
||||||
postToUni({
|
notifyResult({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
result: {
|
result: {
|
||||||
captcha_id: result.captcha_id || captchaId,
|
captcha_id: result.captcha_id || captchaId,
|
||||||
@@ -55,20 +103,38 @@
|
|||||||
gen_time: result.gen_time || '',
|
gen_time: result.gen_time || '',
|
||||||
captcha_output: result.captcha_output || ''
|
captcha_output: result.captcha_output || ''
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
closePage()
|
try {
|
||||||
})
|
if (typeof instance.destroy === 'function') instance.destroy();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
instance.onFail(function () {
|
instance.onFail(function () {
|
||||||
postToUni({ type: 'fail', msg: '人机验证未通过' })
|
failAndNotify('人机验证未通过');
|
||||||
closePage()
|
});
|
||||||
})
|
|
||||||
instance.onError(function () {
|
instance.onError(function () {
|
||||||
postToUni({ type: 'fail', msg: '人机验证加载失败' })
|
failAndNotify('人机验证加载失败');
|
||||||
closePage()
|
});
|
||||||
})
|
instance.onClose(function () {
|
||||||
instance.showCaptcha()
|
setTimeout(function () {
|
||||||
})
|
if (!finished) failAndNotify('已取消人机验证');
|
||||||
})()
|
}, 300);
|
||||||
|
});
|
||||||
|
instance.showCaptcha();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__startGeetest = startCaptcha;
|
||||||
|
|
||||||
|
if (captchaId) {
|
||||||
|
startCaptcha(captchaId);
|
||||||
|
} else if (window.plus) {
|
||||||
|
startCaptcha();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('plusready', function () {
|
||||||
|
startCaptcha();
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -174,8 +174,9 @@ var loadScript = function (url, cb, timeout) {
|
|||||||
script.charset = "UTF-8";
|
script.charset = "UTF-8";
|
||||||
script.async = true;
|
script.async = true;
|
||||||
|
|
||||||
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
|
// 对geetest的静态资源添加 crossOrigin。
|
||||||
if ( /static\.geetest\.com/g.test(url)) {
|
// file:// 本地页(App web-view)带 crossOrigin 会触发 CORS,必须跳过。
|
||||||
|
if ( /static\.geetest\.com/g.test(url) && window.location.protocol !== 'file:') {
|
||||||
script.crossOrigin = "anonymous";
|
script.crossOrigin = "anonymous";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +382,13 @@ window.initGeetest4 = function (userConfig,callback) {
|
|||||||
if (userConfig.https) {
|
if (userConfig.https) {
|
||||||
config.protocol = 'https://';
|
config.protocol = 'https://';
|
||||||
} else if (!userConfig.protocol) {
|
} else if (!userConfig.protocol) {
|
||||||
config.protocol = window.location.protocol + '//';
|
var locProtocol = window.location.protocol;
|
||||||
|
// file:// / app:// 不能沿用当前协议,否则会请求 file://static.geetest.com
|
||||||
|
if (locProtocol === 'http:' || locProtocol === 'https:') {
|
||||||
|
config.protocol = locProtocol + '//';
|
||||||
|
} else {
|
||||||
|
config.protocol = 'https://';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -174,8 +174,9 @@ var loadScript = function (url, cb, timeout) {
|
|||||||
script.charset = "UTF-8";
|
script.charset = "UTF-8";
|
||||||
script.async = true;
|
script.async = true;
|
||||||
|
|
||||||
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
|
// 对geetest的静态资源添加 crossOrigin。
|
||||||
if ( /static\.geetest\.com/g.test(url)) {
|
// file:// 本地页(App web-view)带 crossOrigin 会触发 CORS,必须跳过。
|
||||||
|
if ( /static\.geetest\.com/g.test(url) && window.location.protocol !== 'file:') {
|
||||||
script.crossOrigin = "anonymous";
|
script.crossOrigin = "anonymous";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +382,13 @@ window.initGeetest4 = function (userConfig,callback) {
|
|||||||
if (userConfig.https) {
|
if (userConfig.https) {
|
||||||
config.protocol = 'https://';
|
config.protocol = 'https://';
|
||||||
} else if (!userConfig.protocol) {
|
} else if (!userConfig.protocol) {
|
||||||
config.protocol = window.location.protocol + '//';
|
var locProtocol = window.location.protocol;
|
||||||
|
// file:// / app:// 不能沿用当前协议,否则会请求 file://static.geetest.com
|
||||||
|
if (locProtocol === 'http:' || locProtocol === 'https:') {
|
||||||
|
config.protocol = locProtocol + '//';
|
||||||
|
} else {
|
||||||
|
config.protocol = 'https://';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+38
-7
@@ -36,24 +36,47 @@ function showGeetest4H5(captchaId) {
|
|||||||
reject(new Error('极验 SDK 未加载'))
|
reject(new Error('极验 SDK 未加载'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let settled = false
|
||||||
|
const settle = (fn) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
fn()
|
||||||
|
}
|
||||||
window.initGeetest4(
|
window.initGeetest4(
|
||||||
{
|
{
|
||||||
captchaId,
|
captchaId,
|
||||||
product: 'bind',
|
product: 'bind',
|
||||||
language: 'zh-CN'
|
language: 'zh-CN',
|
||||||
|
https: true
|
||||||
},
|
},
|
||||||
(instance) => {
|
(instance) => {
|
||||||
instance.onSuccess(() => {
|
const teardown = () => {
|
||||||
resolve(normalizeValidate(instance.getValidate(), captchaId))
|
try {
|
||||||
if (typeof instance.destroy === 'function') {
|
if (typeof instance.hideCaptcha === 'function') instance.hideCaptcha()
|
||||||
instance.destroy()
|
if (typeof instance.destroy === 'function') instance.destroy()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
instance.onSuccess(() => {
|
||||||
|
const result = normalizeValidate(instance.getValidate(), captchaId)
|
||||||
|
teardown()
|
||||||
|
settle(() => resolve(result))
|
||||||
})
|
})
|
||||||
instance.onFail(() => {
|
instance.onFail(() => {
|
||||||
reject(new Error('人机验证未通过'))
|
teardown()
|
||||||
|
settle(() => reject(new Error('人机验证未通过')))
|
||||||
})
|
})
|
||||||
instance.onError(() => {
|
instance.onError(() => {
|
||||||
reject(new Error('人机验证加载失败'))
|
teardown()
|
||||||
|
settle(() => reject(new Error('人机验证加载失败')))
|
||||||
|
})
|
||||||
|
instance.onClose?.(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (settled) return
|
||||||
|
teardown()
|
||||||
|
settle(() => reject(new Error('已取消人机验证')))
|
||||||
|
}, 400)
|
||||||
})
|
})
|
||||||
instance.showCaptcha()
|
instance.showCaptcha()
|
||||||
}
|
}
|
||||||
@@ -64,6 +87,14 @@ function showGeetest4H5(captchaId) {
|
|||||||
|
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
function showGeetest4App(captchaId) {
|
function showGeetest4App(captchaId) {
|
||||||
|
// 清理可能残留的原生 Webview
|
||||||
|
if (typeof plus !== 'undefined' && plus.webview) {
|
||||||
|
try {
|
||||||
|
const old = plus.webview.getWebviewById('geetest-captcha-overlay')
|
||||||
|
if (old) old.close('none')
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/login/geetest-webview?captchaId=${encodeURIComponent(captchaId)}`,
|
url: `/pages/login/geetest-webview?captchaId=${encodeURIComponent(captchaId)}`,
|
||||||
|
|||||||
@@ -1,17 +1,89 @@
|
|||||||
/** H5 富文本表格 DOM 操作 */
|
/**
|
||||||
|
* 富文本表格操作与行内样式/属性注入工具
|
||||||
|
* 注入 border="1"、cellpadding="8"、cellspacing="0" 以及 style 属性
|
||||||
|
* 保证在 H5、App-plus 原生 editor、小程序以及 rich-text 中均能拥有清晰的表格边框与样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TABLE_STYLE = 'border-collapse: collapse; width: 100%; border: 1px solid #dcdfe6; margin: 12px 0; table-layout: auto;'
|
||||||
|
const TR_STYLE = 'border-bottom: 1px solid #ebeef5;'
|
||||||
|
const TH_STYLE = 'border: 1px solid #dcdfe6; background-color: #f5f7fa; color: #303133; font-weight: 600; padding: 8px 12px; font-size: 14px; text-align: left; min-width: 44px; word-break: break-word;'
|
||||||
|
const TD_STYLE = 'border: 1px solid #dcdfe6; background-color: #ffffff; color: #303133; padding: 8px 12px; font-size: 14px; vertical-align: top; word-break: break-word; min-width: 44px;'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给 HTML 字符串中的所有 table/tr/th/td 注入高优先级的行内样式与原生 HTML 边框属性
|
||||||
|
* (双重保障,即使 Quill 或原生组件重置了部分 CSS,border="1" 仍能强制渲染出边框)
|
||||||
|
*/
|
||||||
|
export function injectTableInlineStyles(html) {
|
||||||
|
if (!html || typeof html !== 'string') return ''
|
||||||
|
let res = html
|
||||||
|
|
||||||
|
// 注入 table 属性与样式
|
||||||
|
res = res.replace(/<table(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||||||
|
let cleanAttrs = attrs
|
||||||
|
cleanAttrs = cleanAttrs.replace(/\s+(border|cellspacing|cellpadding)\s*=\s*["'][^"']*["']/gi, '')
|
||||||
|
if (/style\s*=\s*["']/i.test(cleanAttrs)) {
|
||||||
|
cleanAttrs = cleanAttrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TABLE_STYLE}"`)
|
||||||
|
} else {
|
||||||
|
cleanAttrs = ` style="${TABLE_STYLE}"` + cleanAttrs
|
||||||
|
}
|
||||||
|
return `<table border="1" cellpadding="8" cellspacing="0"${cleanAttrs}>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注入 tr 样式
|
||||||
|
res = res.replace(/<tr(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||||||
|
if (/style\s*=\s*["']/i.test(attrs)) {
|
||||||
|
return `<tr${attrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TR_STYLE}"`)}>`
|
||||||
|
}
|
||||||
|
return `<tr style="${TR_STYLE}"${attrs}>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注入 th 属性与样式
|
||||||
|
res = res.replace(/<th(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||||||
|
let cleanAttrs = attrs.replace(/\s+border\s*=\s*["'][^"']*["']/gi, '')
|
||||||
|
if (/style\s*=\s*["']/i.test(cleanAttrs)) {
|
||||||
|
cleanAttrs = cleanAttrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TH_STYLE}"`)
|
||||||
|
} else {
|
||||||
|
cleanAttrs = ` style="${TH_STYLE}"` + cleanAttrs
|
||||||
|
}
|
||||||
|
return `<th border="1"${cleanAttrs}>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注入 td 属性与样式
|
||||||
|
res = res.replace(/<td(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||||||
|
let cleanAttrs = attrs.replace(/\s+border\s*=\s*["'][^"']*["']/gi, '')
|
||||||
|
if (/style\s*=\s*["']/i.test(cleanAttrs)) {
|
||||||
|
cleanAttrs = cleanAttrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TD_STYLE}"`)
|
||||||
|
} else {
|
||||||
|
cleanAttrs = ` style="${TD_STYLE}"` + cleanAttrs
|
||||||
|
}
|
||||||
|
return `<td border="1"${cleanAttrs}>`
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export function createEmptyCell(tagName = 'td') {
|
export function createEmptyCell(tagName = 'td') {
|
||||||
const cell = document.createElement(tagName)
|
const cell = document.createElement(tagName)
|
||||||
cell.innerHTML = '<p><br></p>'
|
cell.innerHTML = '<p><br></p>'
|
||||||
|
cell.setAttribute('border', '1')
|
||||||
|
if (tagName === 'th') {
|
||||||
|
cell.style.cssText = TH_STYLE
|
||||||
|
} else {
|
||||||
|
cell.style.cssText = TD_STYLE
|
||||||
|
}
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTableHtml(rows = 3, cols = 3) {
|
export function buildTableHtml(rows = 3, cols = 3) {
|
||||||
let html = '<table><tbody>'
|
let html = `<table border="1" cellpadding="8" cellspacing="0" style="${TABLE_STYLE}"><tbody>`
|
||||||
for (let r = 0; r < rows; r += 1) {
|
for (let r = 0; r < rows; r += 1) {
|
||||||
html += '<tr>'
|
html += `<tr style="${TR_STYLE}">`
|
||||||
for (let c = 0; c < cols; c += 1) {
|
for (let c = 0; c < cols; c += 1) {
|
||||||
html += '<td><p><br></p></td>'
|
if (r === 0) {
|
||||||
|
html += `<th border="1" style="${TH_STYLE}"><p><br></p></th>`
|
||||||
|
} else {
|
||||||
|
html += `<td border="1" style="${TD_STYLE}"><p><br></p></td>`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
html += '</tr>'
|
html += '</tr>'
|
||||||
}
|
}
|
||||||
@@ -32,10 +104,13 @@ export function getCurrentCell(editorEl) {
|
|||||||
export function addRowBefore(cell) {
|
export function addRowBefore(cell) {
|
||||||
const row = cell.closest('tr')
|
const row = cell.closest('tr')
|
||||||
const newRow = row.cloneNode(true)
|
const newRow = row.cloneNode(true)
|
||||||
|
newRow.style.cssText = TR_STYLE
|
||||||
newRow.querySelectorAll('td, th').forEach((item) => {
|
newRow.querySelectorAll('td, th').forEach((item) => {
|
||||||
item.innerHTML = '<p><br></p>'
|
item.innerHTML = '<p><br></p>'
|
||||||
item.removeAttribute('colspan')
|
item.removeAttribute('colspan')
|
||||||
item.removeAttribute('rowspan')
|
item.removeAttribute('rowspan')
|
||||||
|
item.setAttribute('border', '1')
|
||||||
|
item.style.cssText = item.tagName.toLowerCase() === 'th' ? TH_STYLE : TD_STYLE
|
||||||
})
|
})
|
||||||
row.before(newRow)
|
row.before(newRow)
|
||||||
return newRow.cells[cell.cellIndex] || newRow.cells[0]
|
return newRow.cells[cell.cellIndex] || newRow.cells[0]
|
||||||
@@ -44,10 +119,13 @@ export function addRowBefore(cell) {
|
|||||||
export function addRowAfter(cell) {
|
export function addRowAfter(cell) {
|
||||||
const row = cell.closest('tr')
|
const row = cell.closest('tr')
|
||||||
const newRow = row.cloneNode(true)
|
const newRow = row.cloneNode(true)
|
||||||
|
newRow.style.cssText = TR_STYLE
|
||||||
newRow.querySelectorAll('td, th').forEach((item) => {
|
newRow.querySelectorAll('td, th').forEach((item) => {
|
||||||
item.innerHTML = '<p><br></p>'
|
item.innerHTML = '<p><br></p>'
|
||||||
item.removeAttribute('colspan')
|
item.removeAttribute('colspan')
|
||||||
item.removeAttribute('rowspan')
|
item.removeAttribute('rowspan')
|
||||||
|
item.setAttribute('border', '1')
|
||||||
|
item.style.cssText = item.tagName.toLowerCase() === 'th' ? TH_STYLE : TD_STYLE
|
||||||
})
|
})
|
||||||
row.after(newRow)
|
row.after(newRow)
|
||||||
return newRow.cells[cell.cellIndex] || newRow.cells[0]
|
return newRow.cells[cell.cellIndex] || newRow.cells[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user