From 0e797c39f42f08df52c03ceb69be01350c132acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com> Date: Tue, 18 Aug 2026 09:25:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dh5=E8=AE=B0=E4=BA=8B=E6=9C=AC?= =?UTF-8?q?=E4=B8=8D=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uniapp/components/NoteRichEditor.vue | 794 ++++++++++++++---------- uniapp/pages.json | 9 +- uniapp/pages/login/geetest-webview.vue | 105 +++- uniapp/pages/login/login.vue | 4 +- uniapp/pages/tools/notepad/edit.vue | 14 +- uniapp/src/styles/rich-content.scss | 124 ++-- uniapp/static/html/geetest-captcha.html | 156 +++-- uniapp/static/html/gt4.js | 13 +- uniapp/static/js/gt4.js | 13 +- uniapp/utils/geetest.js | 45 +- uniapp/utils/table-editor.js | 86 ++- 11 files changed, 911 insertions(+), 452 deletions(-) diff --git a/uniapp/components/NoteRichEditor.vue b/uniapp/components/NoteRichEditor.vue index 49cd699..e57bf0a 100644 --- a/uniapp/components/NoteRichEditor.vue +++ b/uniapp/components/NoteRichEditor.vue @@ -1,64 +1,104 @@ + + + + diff --git a/uniapp/pages/login/login.vue b/uniapp/pages/login/login.vue index 2febc48..76fc642 100644 --- a/uniapp/pages/login/login.vue +++ b/uniapp/pages/login/login.vue @@ -371,9 +371,11 @@ async function handleLogin() { if (ok) navigateAfterLogin() } catch (err) { const msg = err?.message || '人机验证失败' - if (msg !== '人机验证未通过') { + if (msg !== '人机验证未通过' && msg !== '已取消人机验证') { uni.showToast({ title: msg, icon: 'none' }) } + } finally { + submitting.value = false } return } diff --git a/uniapp/pages/tools/notepad/edit.vue b/uniapp/pages/tools/notepad/edit.vue index a78d82f..6de3f6b 100644 --- a/uniapp/pages/tools/notepad/edit.vue +++ b/uniapp/pages/tools/notepad/edit.vue @@ -49,6 +49,7 @@ import { ref, reactive } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { getNote, createNote, updateNote, deleteNote } from '@/api/note.js' import { formatDate } from '@/utils/date.js' +import { injectTableInlineStyles } from '@/utils/table-editor.js' import NoteRichEditor from '@/components/NoteRichEditor.vue' const noteId = ref('') @@ -81,7 +82,7 @@ async function loadNote(id) { return } form.title = note.title - form.content = note.content + form.content = injectTableInlineStyles(note.content || '') form.pinned = !!note.pinned meta.updatedAt = note.updatedAt } catch (e) { @@ -97,16 +98,17 @@ async function onSave() { } saving.value = true try { + const formattedContent = injectTableInlineStyles(form.content) if (noteId.value) { await updateNote(noteId.value, { title: form.title, - content: form.content, + content: formattedContent, pinned: form.pinned }) } else { await createNote({ title: form.title, - content: form.content, + content: formattedContent, pinned: form.pinned }) } @@ -191,14 +193,16 @@ function onDelete() { padding: 24rpx 28rpx 0 28rpx; overflow: hidden; border-top: 1rpx solid $color-divider; + min-height: 520rpx; } .note-editor { width: 100%; flex: 1; - min-height: 0; + min-height: 440rpx; box-sizing: border-box; - overflow: hidden; + display: flex; + flex-direction: column; background: #f5f7fa; border-radius: 12rpx; padding: 20rpx; diff --git a/uniapp/src/styles/rich-content.scss b/uniapp/src/styles/rich-content.scss index e2a26ab..927b872 100644 --- a/uniapp/src/styles/rich-content.scss +++ b/uniapp/src/styles/rich-content.scss @@ -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, -.rich-content { +.rich-content, +.note-rich-editor { + width: 100%; + box-sizing: border-box; overflow-x: auto; + font-size: 28rpx; + line-height: 1.6; + color: #303133; p { 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, video { max-width: 100%; height: auto; + border-radius: 8rpx; + margin: 8px 0; } blockquote { - border-left: 4px solid #3973ff; + border-left: 4px solid #3c9cff; background-color: #f5f7fa; color: #606266; padding: 8px 16px; margin: 12px 0; + border-radius: 0 8rpx 8rpx 0; } pre { background-color: #f5f7fa; border: 1px solid #e4e7ed; - border-radius: 4px; + border-radius: 8rpx; padding: 12px 16px; margin: 12px 0; overflow-x: auto; @@ -37,37 +115,11 @@ code { background-color: #f5f7fa; border: 1px solid #e4e7ed; - border-radius: 3px; + border-radius: 6rpx; padding: 2px 6px; font-family: Consolas, Monaco, monospace; - font-size: 13px; - } - - 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; - } + font-size: 24rpx; + color: #e6a23c; } ul, @@ -77,7 +129,7 @@ } li { - line-height: 1.8; + line-height: 1.7; margin: 4px 0; } diff --git a/uniapp/static/html/geetest-captcha.html b/uniapp/static/html/geetest-captcha.html index 2c89cc1..c7984cd 100644 --- a/uniapp/static/html/geetest-captcha.html +++ b/uniapp/static/html/geetest-captcha.html @@ -8,67 +8,133 @@ html, body { margin: 0; padding: 0; - background: rgba(0, 0, 0, 0.45); + background: #f5f7fa; 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; } +
正在加载安全验证...
diff --git a/uniapp/static/html/gt4.js b/uniapp/static/html/gt4.js index 6b2f6ae..0ae95ab 100644 --- a/uniapp/static/html/gt4.js +++ b/uniapp/static/html/gt4.js @@ -174,8 +174,9 @@ var loadScript = function (url, cb, timeout) { script.charset = "UTF-8"; script.async = true; - // 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin - if ( /static\.geetest\.com/g.test(url)) { + // 对geetest的静态资源添加 crossOrigin。 + // file:// 本地页(App web-view)带 crossOrigin 会触发 CORS,必须跳过。 + if ( /static\.geetest\.com/g.test(url) && window.location.protocol !== 'file:') { script.crossOrigin = "anonymous"; } @@ -381,7 +382,13 @@ window.initGeetest4 = function (userConfig,callback) { if (userConfig.https) { config.protocol = 'https://'; } 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://'; + } } diff --git a/uniapp/static/js/gt4.js b/uniapp/static/js/gt4.js index 6b2f6ae..0ae95ab 100644 --- a/uniapp/static/js/gt4.js +++ b/uniapp/static/js/gt4.js @@ -174,8 +174,9 @@ var loadScript = function (url, cb, timeout) { script.charset = "UTF-8"; script.async = true; - // 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin - if ( /static\.geetest\.com/g.test(url)) { + // 对geetest的静态资源添加 crossOrigin。 + // file:// 本地页(App web-view)带 crossOrigin 会触发 CORS,必须跳过。 + if ( /static\.geetest\.com/g.test(url) && window.location.protocol !== 'file:') { script.crossOrigin = "anonymous"; } @@ -381,7 +382,13 @@ window.initGeetest4 = function (userConfig,callback) { if (userConfig.https) { config.protocol = 'https://'; } 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://'; + } } diff --git a/uniapp/utils/geetest.js b/uniapp/utils/geetest.js index 75b2301..83bec1e 100644 --- a/uniapp/utils/geetest.js +++ b/uniapp/utils/geetest.js @@ -36,24 +36,47 @@ function showGeetest4H5(captchaId) { reject(new Error('极验 SDK 未加载')) return } + let settled = false + const settle = (fn) => { + if (settled) return + settled = true + fn() + } window.initGeetest4( { captchaId, product: 'bind', - language: 'zh-CN' + language: 'zh-CN', + https: true }, (instance) => { - instance.onSuccess(() => { - resolve(normalizeValidate(instance.getValidate(), captchaId)) - if (typeof instance.destroy === 'function') { - instance.destroy() + const teardown = () => { + try { + if (typeof instance.hideCaptcha === 'function') instance.hideCaptcha() + if (typeof instance.destroy === 'function') instance.destroy() + } catch { + // ignore } + } + instance.onSuccess(() => { + const result = normalizeValidate(instance.getValidate(), captchaId) + teardown() + settle(() => resolve(result)) }) instance.onFail(() => { - reject(new Error('人机验证未通过')) + teardown() + settle(() => reject(new Error('人机验证未通过'))) }) instance.onError(() => { - reject(new Error('人机验证加载失败')) + teardown() + settle(() => reject(new Error('人机验证加载失败'))) + }) + instance.onClose?.(() => { + setTimeout(() => { + if (settled) return + teardown() + settle(() => reject(new Error('已取消人机验证'))) + }, 400) }) instance.showCaptcha() } @@ -64,6 +87,14 @@ function showGeetest4H5(captchaId) { // #ifdef APP-PLUS 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) => { uni.navigateTo({ url: `/pages/login/geetest-webview?captchaId=${encodeURIComponent(captchaId)}`, diff --git a/uniapp/utils/table-editor.js b/uniapp/utils/table-editor.js index 6bf618b..aa3b329 100644 --- a/uniapp/utils/table-editor.js +++ b/uniapp/utils/table-editor.js @@ -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(/]*)?>/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 `` + }) + + // 注入 tr 样式 + res = res.replace(/]*)?>/gi, (match, attrs = '') => { + if (/style\s*=\s*["']/i.test(attrs)) { + return ` `style="${s}; ${TR_STYLE}"`)}>` + } + return `` + }) + + // 注入 th 属性与样式 + res = res.replace(/]*)?>/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 `
` + }) + + // 注入 td 属性与样式 + res = res.replace(/]*)?>/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 `` + }) + + return res +} export function createEmptyCell(tagName = 'td') { const cell = document.createElement(tagName) cell.innerHTML = '


' + cell.setAttribute('border', '1') + if (tagName === 'th') { + cell.style.cssText = TH_STYLE + } else { + cell.style.cssText = TD_STYLE + } return cell } export function buildTableHtml(rows = 3, cols = 3) { - let html = '' + let html = `
` for (let r = 0; r < rows; r += 1) { - html += '' + html += `` for (let c = 0; c < cols; c += 1) { - html += '' + if (r === 0) { + html += `` + } else { + html += `` + } } html += '' } @@ -32,10 +104,13 @@ export function getCurrentCell(editorEl) { export function addRowBefore(cell) { const row = cell.closest('tr') const newRow = row.cloneNode(true) + newRow.style.cssText = TR_STYLE newRow.querySelectorAll('td, th').forEach((item) => { item.innerHTML = '


' item.removeAttribute('colspan') item.removeAttribute('rowspan') + item.setAttribute('border', '1') + item.style.cssText = item.tagName.toLowerCase() === 'th' ? TH_STYLE : TD_STYLE }) row.before(newRow) return newRow.cells[cell.cellIndex] || newRow.cells[0] @@ -44,10 +119,13 @@ export function addRowBefore(cell) { export function addRowAfter(cell) { const row = cell.closest('tr') const newRow = row.cloneNode(true) + newRow.style.cssText = TR_STYLE newRow.querySelectorAll('td, th').forEach((item) => { item.innerHTML = '


' item.removeAttribute('colspan') item.removeAttribute('rowspan') + item.setAttribute('border', '1') + item.style.cssText = item.tagName.toLowerCase() === 'th' ? TH_STYLE : TD_STYLE }) row.after(newRow) return newRow.cells[cell.cellIndex] || newRow.cells[0]