| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795 |
- <!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Summernote富文本编辑器</title>
- <th:block th:include="include :: header('Summernote富文本编辑器')" />
- <th:block th:include="include :: summernote-css" />
- <style>
- .result-panel {
- border-left: 1px solid #e7eaec;
- height: 100%;
- padding: 15px;
- background-color: #f9f9f9;
- }
- .error-item {
- margin-bottom: 20px;
- padding: 15px;
- border-radius: 5px;
- background-color: #fff;
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- .error-title {
- font-weight: bold;
- margin-bottom: 12px;
- font-size: 16px;
- color: #ed5565;
- }
- .common-error .error-title {
- color: #ed5565;
- }
- .political-error .error-title {
- color: #ed5565;
- }
- .suggestion-table {
- width: 100%;
- margin-bottom: 10px;
- border-collapse: collapse;
- word-break: break-all;
- word-wrap: break-word;
- white-space: normal;
- }
- .suggestion-table th, .suggestion-table td {
- padding: 8px;
- border: 1px solid #ddd;
- text-align: left;
- }
- .suggestion-table th {
- width: 80px;
- background-color: #f9f9f9;
- }
- .confidence {
- font-size: 12px;
- color: #888;
- margin-top: 5px;
- }
- .political-warning {
- font-weight: bold;
- }
- .error-highlight {
- background-color: #ffcccc !important;
- color: #ed5565 !important;
- border-bottom: 1px dashed #ed5565 !important;
- padding: 0 2px !important;
- }
- .error-content a {
- display: inline-block;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- vertical-align: bottom;
- }
- /* 编辑器容器固定高度 */
- .ibox-content.no-padding {
- height: 650px;
- display: flex;
- flex-direction: column;
- position: relative;
- }
- /* 编辑器区域自动滚动 */
- .summernote {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
- /* Summernote编辑器包装器 */
- .note-editor {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
- /* Summernote编辑区域 */
- .note-editable {
- flex: 1;
- overflow-y: auto !important;
- min-height: 200px;
- }
- /* 底部按钮固定在容器底部 */
- .editor-footer {
- position: sticky;
- bottom: 0;
- background-color: #fff;
- z-index: 100;
- padding: 15px;
- border-top: 1px solid #e7eaec;
- text-align: center;
- }
- /* 校对结果面板样式 - 电脑版 */
- .col-sm-4 {
- position: fixed !important;
- right: 0px !important;
- z-index: 1000 !important;
- max-height: 80vh !important;
- }
- .col-sm-4 .ibox-content {
- max-height: calc(96vh - 50px) !important;
- overflow-y: auto !important;
- }
- /* 新增误报按钮样式 */
- .false-positive-btn {
- font-size: 12px;
- background-color: #f8f9fa;
- border: 1px solid #ddd;
- border-radius: 3px;
- color: #6c757d;
- cursor: pointer;
- transition: all 0.2s;
- display: block;
- text-align: center;
- padding: 4px 8px;
- margin-top: 8px;
- }
- .false-positive-btn:hover {
- background-color: #e9ecef;
- color: #495057;
- }
- .false-positive-btn.reported {
- color: #155724;
- border-color: #c3e6cb;
- cursor: default;
- }
- .false-positive-btn i {
- margin-right: 4px;
- }
- .error-actions {
- margin-bottom: 10px;
- text-align: right;
- }
- .error-type-divider {
- margin: 15px 0;
- border-top: 1px dashed #e7eaec;
- }
- /* 加载状态样式 */
- .loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- }
- /* ========== 手机版适配样式 ========== */
- @media (max-width: 768px) {
- /* 整体布局调整 */
- .wrapper-content {
- padding: 10px;
- }
- .row {
- display: block;
- margin: 0;
- }
- /* 编辑器区域 - 全宽度 */
- .col-sm-8 {
- width: 100%;
- padding: 0;
- margin-bottom: 0;
- }
- /* 结果面板 - 手机版改为正常文档流,在编辑器下方 */
- .col-sm-4 {
- position: static !important;
- width: 100% !important;
- max-height: none !important;
- margin-top: 20px;
- }
- .col-sm-4 .ibox-content {
- max-height: none !important;
- height: auto;
- min-height: 300px;
- max-height: 500px;
- overflow-y: auto;
- }
- /* 编辑器高度调整 */
- .ibox-content.no-padding {
- height: 500px;
- }
- /* 按钮调整 */
- .editor-footer .btn {
- padding: 10px 15px;
- font-size: 14px;
- margin: 0 5px;
- }
- /* 表格字体调整 */
- .suggestion-table {
- font-size: 14px;
- }
- .suggestion-table th,
- .suggestion-table td {
- padding: 10px 8px;
- }
- /* 错误项间距调整 */
- .error-item {
- padding: 12px;
- margin-bottom: 15px;
- }
- .error-title {
- font-size: 16px;
- }
- /* Summernote 工具栏优化 */
- .note-toolbar {
- padding: 8px !important;
- }
- .note-toolbar .btn-group {
- margin-bottom: 5px;
- }
- .note-toolbar .btn {
- padding: 8px 10px;
- font-size: 13px;
- }
- .note-editable {
- font-size: 16px; /* 手机端字体放大 */
- }
- /* 手机版底部间距 */
- .wrapper {
- padding-bottom: 20px;
- }
- }
- /* 超小屏幕设备 */
- @media (max-width: 480px) {
- .ibox-content.no-padding {
- height: 400px;
- }
- .editor-footer .btn {
- padding: 8px 12px;
- font-size: 13px;
- margin: 0 3px;
- }
- .suggestion-table {
- font-size: 13px;
- }
- .error-content h4 {
- font-size: 15px;
- }
- .col-sm-4 .ibox-content {
- min-height: 250px;
- max-height: 400px;
- }
- }
- </style>
- </head>
- <body class="gray-bg">
- <div class="wrapper wrapper-content">
- <div class="row">
- <!-- 编辑器区域 -->
- <div class="col-sm-8">
- <div class="ibox float-e-margins">
- <div class="ibox-content no-padding" style="position: relative;">
- <!-- 加载遮罩层 -->
- <div id="loadingOverlay" class="loading-overlay" style="display: none;">
- <div style="text-align: center;">
- <i class="fa fa-spinner fa-spin fa-3x fa-fw" style="color: #1ab394;"></i>
- <p style="margin-top: 15px; font-size: 16px;">校对中,请稍候...</p>
- </div>
- </div>
- <div class="summernote"></div>
- <div class="editor-footer">
- <button id="checkBtn" class="btn btn-primary btn-sm">
- <i class="fa fa-check"></i> 校对
- </button>
- <button id="resetBtn" class="btn btn-warning btn-sm">
- <i class="fa fa-refresh"></i> 重置
- </button>
- <button id="clearBtn" class="btn btn-danger btn-sm">
- <i class="fa fa-trash"></i> 清空
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 结果面板区域 -->
- <div class="col-sm-4" id="resultsPanel">
- <!-- 新增:独立的安全警告模块 -->
- <div class="security-alert-panel" style="
- background: linear-gradient(135deg, #fff5f5, #ffe6e6);
- border: 2px solid #ff4d4f;
- border-radius: 8px;
- margin-bottom: 15px;
- padding: 15px;
- box-shadow: 0 3px 10px rgba(255, 77, 79, 0.2);
- ">
- <div style="
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- ">
- <i class="fa fa-exclamation-triangle" style="
- color: #ff4d4f;
- font-size: 20px;
- flex-shrink: 0;
- "></i>
- <div style="
- color: #ff4d4f;
- font-weight: bold;
- font-size: 15px;
- text-align: center;
- line-height: 1.4;
- ">
- 严禁处理涉密信息<br>
- <span style="
- font-size: 12px;
- font-weight: normal;
- color: #d9363e;
- margin-top: 3px;
- display: block;
- ">
- </span>
- </div>
- </div>
- </div>
- <div class="ibox float-e-margins fixed-result-panel">
- <div class="ibox-title">
- <h5>校对结果</h5>
- </div>
- <div class="ibox-content result-panel">
- <div id="checkResults">
- <div class="text-muted" style="text-align: center; padding: 20px;">
- <i class="fa fa-info-circle fa-2x"></i>
- <p>点击"校对"按钮检查文档内容</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <th:block th:include="include :: footer" />
- <th:block th:include="include :: summernote-js" />
- <script>
- var initialContent = '';
- var prefix = ctx + "project/jiaodui";
- var wordResults = ctx + "project/wordresults";
- var currentErrors = [];
- var falsePositiveErrors = [];
- $(document).ready(function () {
- // Summernote 初始化
- $('.summernote').summernote({
- lang: 'zh-CN',
- height: 460,
- toolbar: [
- ['style', ['bold', 'italic', 'underline', 'clear']],
- ['font', ['strikethrough', 'superscript', 'subscript']],
- ['fontsize', ['fontsize']],
- ['color', ['color']],
- ['para', ['ul', 'ol', 'paragraph']],
- ['height', ['height']],
- ['insert', ['picture', 'link', 'video', 'table', 'hr']],
- ['view', ['fullscreen', 'codeview', 'help']]
- ]
- });
- // 显示加载状态
- function showLoading() {
- $('#loadingOverlay').show();
- $('#checkBtn').prop('disabled', true);
- }
- // 隐藏加载状态
- function hideLoading() {
- $('#loadingOverlay').hide();
- $('#checkBtn').prop('disabled', false);
- }
- // 清除所有错误高亮
- function clearErrorHighlights() {
- var content = $('.summernote').summernote('code');
- content = content.replace(/<span class="error-highlight"[^>]*>(.*?)<\/span>/gi, '$1');
- $('.summernote').summernote('code', content);
- }
- // 高亮显示错误词 - 完整版
- function highlightErrors(errors) {
- var content = $('.summernote').summernote('code');
- // 先清除旧的高亮
- content = clearErrorHighlightsFromContent(content);
- // 分离有位置信息和无位置信息的错误
- var errorsWithPosition = [];
- var errorsWithoutPosition = [];
- errors.forEach(function(error) {
- if (falsePositiveErrors.includes(error.id)) {
- return;
- }
- // 检查是否有有效的起止位置信息
- if (isValidPosition(error)) {
- errorsWithPosition.push(error);
- } else {
- errorsWithoutPosition.push(error);
- }
- });
- console.log('有位置信息的错误:', errorsWithPosition.length);
- console.log('无位置信息的错误:', errorsWithoutPosition.length);
- // 先处理有位置信息的错误(从后往前)
- if (errorsWithPosition.length > 0) {
- content = highlightErrorsWithPosition(content, errorsWithPosition);
- }
- // 再处理无位置信息的错误
- if (errorsWithoutPosition.length > 0) {
- content = highlightErrorsWithoutPosition(content, errorsWithoutPosition);
- }
- $('.summernote').summernote('code', content);
- }
- // 检查位置信息是否有效
- function isValidPosition(error) {
- return error.startpos !== undefined &&
- error.endpos !== undefined &&
- error.startpos >= 0 &&
- error.endpos > error.startpos &&
- error.wrongWord;
- }
- // 从内容中清除高亮(不更新编辑器)
- function clearErrorHighlightsFromContent(content) {
- return content.replace(/<span class="error-highlight"[^>]*>(.*?)<\/span>/gi, '$1');
- }
- // 高亮有位置信息的错误
- function highlightErrorsWithPosition(htmlContent, errors) {
- var plainText = getPlainText(htmlContent);
- console.log('纯文本长度:', plainText.length);
- console.log('纯文本内容:', plainText);
- // 按位置从后往前高亮,避免位置偏移
- errors.sort((a, b) => b.startpos - a.startpos).forEach(function(error, index) {
- console.log(`处理错误 ${index + 1}:`, error);
- try {
- var newContent = highlightSingleErrorWithPosition(htmlContent, plainText, error);
- if (newContent) {
- htmlContent = newContent;
- } else {
- // 位置高亮失败,回退到全局替换
- console.warn('位置高亮失败,使用全局替换:', error.wrongWord);
- htmlContent = highlightWithGlobalReplace(htmlContent, error);
- }
- } catch (e) {
- console.error('位置高亮异常,使用全局替换:', error.wrongWord, e);
- htmlContent = highlightWithGlobalReplace(htmlContent, error);
- }
- });
- return htmlContent;
- }
- // 高亮单个有位置信息的错误
- function highlightSingleErrorWithPosition(htmlContent, plainText, error) {
- var errorText = plainText.substring(error.startpos, error.endpos);
- console.log('期望的错误文本:', error.wrongWord);
- console.log('实际提取的文本:', errorText);
- console.log('位置范围:', error.startpos, '-', error.endpos);
- // 验证提取的文本是否匹配
- if (errorText !== error.wrongWord) {
- console.warn('位置信息不准确,期望:', error.wrongWord, '实际:', errorText);
- return null;
- }
- // 使用DOM方式精确定位
- var tempDiv = document.createElement('div');
- tempDiv.innerHTML = htmlContent;
- var result = findAndHighlightTextNode(tempDiv, error, plainText);
- if (result.found) {
- return tempDiv.innerHTML;
- } else {
- return null;
- }
- }
- // 查找并高亮文本节点
- function findAndHighlightTextNode(element, error, plainText) {
- var walker = document.createTreeWalker(
- element,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
- var currentGlobalPos = 0;
- var node = walker.nextNode();
- while (node) {
- var nodeText = node.textContent;
- var nodeLength = nodeText.length;
- // 检查错误是否在这个文本节点中
- if (error.startpos >= currentGlobalPos && error.endpos <= currentGlobalPos + nodeLength) {
- var nodeStart = error.startpos - currentGlobalPos;
- var nodeEnd = error.endpos - currentGlobalPos;
- // 验证节点内的文本是否匹配
- var nodeErrorText = nodeText.substring(nodeStart, nodeEnd);
- if (nodeErrorText === error.wrongWord) {
- // 创建高亮span
- var span = document.createElement('span');
- span.className = 'error-highlight';
- span.setAttribute('data-error-id', error.id);
- span.textContent = nodeErrorText;
- // 替换文本节点
- var beforeText = nodeText.substring(0, nodeStart);
- var afterText = nodeText.substring(nodeEnd);
- var parent = node.parentNode;
- if (beforeText) {
- parent.insertBefore(document.createTextNode(beforeText), node);
- }
- parent.insertBefore(span, node);
- if (afterText) {
- parent.insertBefore(document.createTextNode(afterText), node);
- }
- parent.removeChild(node);
- return { found: true };
- }
- }
- currentGlobalPos += nodeLength;
- node = walker.nextNode();
- }
- return { found: false };
- }
- // 高亮无位置信息的错误(全局替换)
- function highlightErrorsWithoutPosition(content, errors) {
- errors.forEach(function(error) {
- content = highlightWithGlobalReplace(content, error);
- });
- return content;
- }
- // 全局文本替换高亮
- function highlightWithGlobalReplace(content, error) {
- var escapedWord = escapeRegExp(error.wrongWord);
- var regex = new RegExp(escapedWord, 'g');
- return content.replace(regex, function(match) {
- return '<span class="error-highlight" data-error-id="' + error.id + '">' + match + '</span>';
- });
- }
- // 转义正则表达式特殊字符
- function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
- // 获取纯文本
- function getPlainText(html) {
- var tempDiv = document.createElement('div');
- tempDiv.innerHTML = html;
- return tempDiv.textContent || tempDiv.innerText || "";
- }
- // 标记误报
- function markAsFalsePositive(id, wrongWord, correctWord, button) {
- falsePositiveErrors.push(id);
- $(button).addClass('reported').html('<i class="fa fa-check"></i> 已标记').prop('disabled', true);
- clearErrorHighlights();
- highlightErrors(currentErrors);
- var data = { "id": id, "res": 1 };
- $.get(wordResults + "/edit", data, function(result) {
- if (result.code == web_status.SUCCESS) {
- let errorData = result.data;
- }
- });
- }
- // 使用事件委托处理误报按钮点击
- $(document).on('click', '.false-positive-btn', function(e) {
- e.preventDefault();
- if ($(this).prop('disabled')) {
- return;
- }
- var wrongWord = $(this).data('wrong');
- var correctWord = $(this).data('correct');
- var id = $(this).data('id');
- markAsFalsePositive(id, wrongWord, correctWord, this);
- });
- // 校对功能
- $('#checkBtn').click(function() {
- clearErrorHighlights();
- var content = $('.summernote').summernote('code');
- if (content == '' || content == null || content == '<p><br></p>' || $.trim(content.replace(/<[^>]*>/g, '')) === '') {
- alert('请输入校对内容');
- return false;
- }
- showLoading();
- var requestData = {
- content: content,
- title: "文本校对",
- type: "1"
- };
- $.ajax({
- url: prefix + "/jiaodui",
- type: 'POST',
- contentType: 'application/json',
- data: JSON.stringify(requestData),
- success: function(result) {
- if (result.code == 0) {
- let errorData = result.data;
- currentErrors = errorData;
- console.log('收到错误数据:', errorData);
- if (errorData && errorData.length > 0) {
- highlightErrors(errorData);
- }
- let resultHtml = '';
- const errorsByType = {};
- if (errorData && errorData.length > 0) {
- errorData.forEach(error => {
- if (!errorsByType[error.errorType]) {
- errorsByType[error.errorType] = [];
- }
- errorsByType[error.errorType].push(error);
- });
- Object.keys(errorsByType).forEach(errorType => {
- const errors = errorsByType[errorType];
- let errorTypeHtml = '';
- errors.forEach((error, index) => {
- const isFalsePositive = falsePositiveErrors.includes(error.id);
- errorTypeHtml += `
- <div class="error-content">
- <h4>${index + 1}、${error.wrongWord}</h4>
- <table class="suggestion-table">
- <tr>
- <th>修改建议</th>
- <td>${error.correctWord}</td>
- </tr>
- ${error.source ? `<tr><th>参考依据</th><td>${error.source}</td></tr>` : ''}
- </table>
- <div class="error-actions">
- <button class="false-positive-btn" data-id="${error.id}" data-wrong="${error.wrongWord}" data-correct="${error.correctWord}" ${isFalsePositive ? 'disabled' : ''}>
- <i class="fa ${isFalsePositive ? 'fa-check' : 'fa-flag'}"></i> ${isFalsePositive ? '已标记' : '误报'}
- </button>
- </div>
- </div>
- `;
- });
- resultHtml += `
- <div class="error-item">
- <div class="error-title">${errorType}(${errors.length})</div>
- ${errorTypeHtml}
- </div>
- <div class="error-type-divider"></div>
- `;
- });
- resultHtml = resultHtml.replace(/<div class="error-type-divider"><\/div>$/, '');
- } else {
- resultHtml = `
- <div class="text-muted" style="text-align: center; padding: 20px;">
- <i class="fa fa-check-circle fa-2x" style="color:#1ab394"></i>
- <p>未发现需要修改的内容</p>
- </div>
- `;
- }
- $('#checkResults').html(resultHtml);
- // 手机版自动滚动到结果区域
- if ($(window).width() <= 768) {
- $('html, body').animate({
- scrollTop: $('#resultsPanel').offset().top
- }, 500);
- }
- } else {
- $.modal.alertError(result.msg);
- }
- },
- error: function(xhr, status, error) {
- var errorMsg = '请求失败: ';
- if (xhr.status === 413) {
- errorMsg = '文本内容过大,请减少内容后重试';
- } else if (xhr.status === 0) {
- errorMsg = '网络连接失败,请检查网络后重试';
- } else {
- errorMsg += error;
- }
- $.modal.alertError(errorMsg);
- },
- complete: function() {
- hideLoading();
- }
- });
- });
- $('#resetBtn').click(function() {
- clearErrorHighlights();
- $('.summernote').summernote('code', initialContent);
- $('#checkResults').html('<div class="text-muted" style="text-align: center; padding: 20px;"><i class="fa fa-info-circle fa-2x"></i><p>内容已重置,请重新校对</p></div>');
- toastr.success('内容已重置为初始状态');
- currentErrors = [];
- falsePositiveErrors = [];
- });
- $('#clearBtn').click(function() {
- if(confirm('确定要清空编辑器内容吗?')) {
- clearErrorHighlights();
- $('.summernote').summernote('reset');
- $('#checkResults').html('<div class="text-muted" style="text-align: center; padding: 20px;"><i class="fa fa-info-circle fa-2x"></i><p>内容已清空,请重新校对</p></div>');
- toastr.info('编辑器内容已清空');
- currentErrors = [];
- falsePositiveErrors = [];
- }
- });
- });
- </script>
- </body>
- </html>
|