新增:JSON 文件格式化预览功能 (#685)
Some checks failed
Java CI with Maven / build (push) Has been cancelled

This commit is contained in:
kl
2025-10-11 11:52:49 +08:00
committed by GitHub
parent 6cdbf92fb0
commit a9787b0add
4 changed files with 248 additions and 1 deletions

View File

@@ -22,6 +22,7 @@ public enum FileType {
MEDIACONVERT("mediaFilePreviewImpl"),
MARKDOWN("markdownFilePreviewImpl"),
XML("xmlFilePreviewImpl"),
JSON("jsonFilePreviewImpl"),
CAD("cadFilePreviewImpl"),
TIFF("tiffFilePreviewImpl"),
OFD("ofdFilePreviewImpl"),
@@ -44,12 +45,13 @@ public enum FileType {
private static final String[] DCM_TYPES = {"dcm"};
private static final String[] DRAWIO_TYPES = {"drawio"};
private static final String[] XML_TYPES = {"xml","xbrl"};
private static final String[] JSON_TYPES = {"json"};
private static final String[] TIFF_TYPES = {"tif", "tiff"};
private static final String[] OFD_TYPES = {"ofd"};
private static final String[] SVG_TYPES = {"svg"};
private static final String[] CAD_TYPES = {"dwg", "dxf", "dwf", "iges", "igs", "dwt", "dng", "ifc", "dwfx", "stl", "cf2", "plt"};
private static final String[] SSIM_TEXT_TYPES = ConfigConstants.getSimText();
private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "json", "h", "cpp", "cs", "aspx", "jsp", "sql"};
private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "h", "cpp", "cs", "aspx", "jsp", "sql"};
private static final String[] MEDIA_TYPES = ConfigConstants.getMedia();
public static final String[] MEDIA_CONVERT_TYPES = ConfigConstants.getConvertMedias();
private static final Map<String, FileType> FILE_TYPE_MAPPER = new HashMap<>();
@@ -109,6 +111,9 @@ public enum FileType {
for (String xml : XML_TYPES) {
FILE_TYPE_MAPPER.put(xml, FileType.XML);
}
for (String json : JSON_TYPES) {
FILE_TYPE_MAPPER.put(json, FileType.JSON);
}
FILE_TYPE_MAPPER.put("md", FileType.MARKDOWN);
FILE_TYPE_MAPPER.put("pdf", FileType.PDF);
FILE_TYPE_MAPPER.put("bpmn", FileType.BPMN);

View File

@@ -26,6 +26,7 @@ public interface FilePreview {
String CODE_FILE_PREVIEW_PAGE = "code";
String EXEL_FILE_PREVIEW_PAGE = "html";
String XML_FILE_PREVIEW_PAGE = "xml";
String JSON_FILE_PREVIEW_PAGE = "json";
String MARKDOWN_FILE_PREVIEW_PAGE = "markdown";
String BPMN_FILE_PREVIEW_PAGE = "bpmn";
String DCM_FILE_PREVIEW_PAGE = "dcm";

View File

@@ -0,0 +1,27 @@
package cn.keking.service.impl;
import cn.keking.model.FileAttribute;
import cn.keking.service.FilePreview;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
/**
* @author kl (http://kailing.pub)
* @since 2025/01/11
* JSON 文件预览处理实现
*/
@Service
public class JsonFilePreviewImpl implements FilePreview {
private final SimTextFilePreviewImpl simTextFilePreview;
public JsonFilePreviewImpl(SimTextFilePreviewImpl simTextFilePreview) {
this.simTextFilePreview = simTextFilePreview;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
simTextFilePreview.filePreviewHandle(url, model, fileAttribute);
return JSON_FILE_PREVIEW_PAGE;
}
}

View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0">
<title>JSON文件预览</title>
<#include "*/commonHeader.ftl">
<script src="js/jquery-3.6.1.min.js" type="text/javascript"></script>
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css"/>
<script src="bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="js/base64.min.js" type="text/javascript"></script>
<style>
body {
font-family: 'Courier New', Courier, monospace;
}
.container {
max-width: 100%;
padding: 20px;
}
.panel-body {
padding: 0;
}
#json {
padding: 20px;
background-color: #f8f9fa;
overflow-x: auto;
}
#text_view {
padding: 20px;
background-color: #ffffff;
overflow-x: auto;
}
pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 14px;
line-height: 1.6;
}
.json-key {
color: #881391;
font-weight: bold;
}
.json-string {
color: #1A1AA6;
}
.json-number {
color: #1C00CF;
}
.json-boolean {
color: #0D22FF;
font-weight: bold;
}
.json-null {
color: #808080;
font-weight: bold;
}
.btn-group {
margin-bottom: 10px;
}
.view-mode-btn {
min-width: 100px;
}
</style>
</head>
<body>
<input hidden id="textData" value="${textData}"/>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
${file.name}
</h4>
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary view-mode-btn" id="formatted_btn">格式化视图</button>
<button type="button" class="btn btn-default view-mode-btn" id="raw_btn">原始文本</button>
</div>
</div>
<div class="panel-body">
<div id="json"></div>
<div id="text_view" style="display:none;"></div>
</div>
</div>
</div>
<script>
/**
* 初始化
*/
window.onload = function () {
initWaterMark();
loadJsonData();
}
/**
* HTML 反转义(用于还原后端转义的内容)
*/
function htmlUnescape(str) {
if (!str || str.length === 0) return "";
var s = str;
s = s.replace(/&quot;/g, '"');
s = s.replace(/&#39;/g, "'");
s = s.replace(/&lt;/g, "<");
s = s.replace(/&gt;/g, ">");
s = s.replace(/&amp;/g, "&");
return s;
}
/**
* HTML 转义(用于安全显示)
*/
function htmlEscape(str) {
if (!str || str.length === 0) return "";
var s = str;
s = s.replace(/&/g, "&amp;");
s = s.replace(/</g, "&lt;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&#39;");
return s;
}
/**
* 移除 BOM (Byte Order Mark)
*/
function removeBOM(str) {
if (str.charCodeAt(0) === 0xFEFF) {
return str.substring(1);
}
return str;
}
/**
* JSON 语法高亮
*/
function syntaxHighlight(json) {
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'json-number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'json-key';
} else {
cls = 'json-string';
}
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
/**
* 加载 JSON 数据
*/
function loadJsonData() {
try {
var textData = Base64.decode($("#textData").val());
// 1. 先反转义 HTML 实体(因为后端已经转义过)
textData = htmlUnescape(textData);
// 2. 移除 BOM
textData = removeBOM(textData);
// 保存原始文本(用于显示时再次转义以保证安全)
window.rawText = "<pre>" + htmlEscape(textData) + "</pre>";
// 尝试解析并格式化 JSON
try {
var jsonObj = JSON.parse(textData);
var formattedJson = JSON.stringify(jsonObj, null, 4);
window.formattedJson = "<pre>" + syntaxHighlight(formattedJson) + "</pre>";
// 默认显示格式化视图
$("#json").html(window.formattedJson);
} catch (e) {
// 如果不是有效的 JSON显示错误并回退到原始文本
window.formattedJson = "<div class='alert alert-warning'>JSON 解析失败: " + htmlEscape(e.message) + "</div>" + window.rawText;
$("#json").html(window.formattedJson);
}
} catch (e) {
$("#json").html("<div class='alert alert-danger'>文件加载失败: " + htmlEscape(e.message) + "</div>");
}
}
/**
* 按钮点击事件
*/
$(function () {
$("#formatted_btn").click(function () {
$("#json").show();
$("#text_view").hide();
$("#json").html(window.formattedJson);
$("#formatted_btn").removeClass("btn-default").addClass("btn-primary");
$("#raw_btn").removeClass("btn-primary").addClass("btn-default");
});
$("#raw_btn").click(function () {
$("#json").hide();
$("#text_view").show();
$("#text_view").html(window.rawText);
$("#raw_btn").removeClass("btn-default").addClass("btn-primary");
$("#formatted_btn").removeClass("btn-primary").addClass("btn-default");
});
});
</script>
</body>
</html>