2025-05-29 14:04:05 +08:00
|
|
|
|
import QtQuick 2.15
|
|
|
|
|
|
import QtQuick.Controls 2.15
|
|
|
|
|
|
import QtQuick.Layouts 1.15
|
|
|
|
|
|
import JsonEditor 1.0
|
2025-09-15 09:46:04 +08:00
|
|
|
|
import HuskarUI.Basic 1.0
|
2025-05-29 14:04:05 +08:00
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
|
width: 600
|
|
|
|
|
|
height: 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
property var projectConfigStr: null // 初始化为空对象
|
|
|
|
|
|
property var saveFunction: function(config){}
|
|
|
|
|
|
property var errorLine: null
|
|
|
|
|
|
property var errorPosition : null
|
|
|
|
|
|
|
|
|
|
|
|
// JSON 处理器
|
|
|
|
|
|
JsonProcessor {
|
|
|
|
|
|
id: jsonProcessor
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将 UTF-8 字节偏移量转换为 UTF-16 代码单元索引
|
|
|
|
|
|
function utf8OffsetToUtf16Index(text, utf8Offset) {
|
|
|
|
|
|
var utf16Index = 0;
|
|
|
|
|
|
var utf8BytesCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < text.length; i++) {
|
|
|
|
|
|
var charCode = text.charCodeAt(i);
|
|
|
|
|
|
var charUtf8Length = 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (charCode <= 0x7F) {
|
|
|
|
|
|
charUtf8Length = 1;
|
|
|
|
|
|
} else if (charCode <= 0x7FF) {
|
|
|
|
|
|
charUtf8Length = 2;
|
|
|
|
|
|
} else if (charCode <= 0xFFFF) {
|
|
|
|
|
|
charUtf8Length = 3;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
charUtf8Length = 4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (utf8BytesCount + charUtf8Length > utf8Offset) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
utf8BytesCount += charUtf8Length;
|
|
|
|
|
|
utf16Index++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return utf16Index;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getErrorLine(text, offset) {
|
|
|
|
|
|
var tbuf = text.slice(0,offset);
|
|
|
|
|
|
var lines = tbuf.split('\n')
|
|
|
|
|
|
if(lines.length > 0)return lines.length
|
|
|
|
|
|
return -1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Rectangle{
|
|
|
|
|
|
id:header_area
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
anchors.bottomMargin: 40
|
|
|
|
|
|
color:"transparent"
|
|
|
|
|
|
radius:8
|
2025-09-15 09:46:04 +08:00
|
|
|
|
border.color: HusTheme.isDark ? "#23272e" : "#f0f4f7"
|
2025-05-29 14:04:05 +08:00
|
|
|
|
border.width: 2
|
|
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
|
id: textScrollView
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
clip: true
|
|
|
|
|
|
TextArea {
|
|
|
|
|
|
id: jsonEditor
|
|
|
|
|
|
font.family: "Consolas"
|
|
|
|
|
|
font.pixelSize: 14
|
|
|
|
|
|
wrapMode: TextArea.Wrap
|
|
|
|
|
|
placeholderText: "请输入JSON内容..."
|
|
|
|
|
|
selectByMouse: true
|
|
|
|
|
|
selectionColor: "#0078d4" // 选中区域颜色(Fluent 主题蓝)
|
|
|
|
|
|
text:projectConfigStr
|
|
|
|
|
|
|
|
|
|
|
|
JsonHighlighter {
|
|
|
|
|
|
document: jsonEditor.textDocument
|
|
|
|
|
|
}
|
|
|
|
|
|
onTextChanged: {
|
|
|
|
|
|
var result = jsonProcessor.validateJson(jsonEditor.text)
|
|
|
|
|
|
if (result.hasError) {
|
|
|
|
|
|
var utf16Index = utf8OffsetToUtf16Index(jsonEditor.text, result.errorOffset);
|
|
|
|
|
|
errorPosition = utf16Index
|
|
|
|
|
|
// 计算错误行号
|
|
|
|
|
|
errorLine = getErrorLine(jsonEditor.text, utf16Index)
|
|
|
|
|
|
errorLabel.text = "❌ JSON语法错误" + `第 ${errorLine} 行出错:${result.errorMessage}`
|
|
|
|
|
|
errorCanvas.requestPaint()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
errorLine = -1
|
|
|
|
|
|
errorPosition = -1
|
|
|
|
|
|
}
|
|
|
|
|
|
errorLabel.visible = (errorLine !== -1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Canvas {
|
|
|
|
|
|
id: errorCanvas
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
visible: errorPosition !== -1
|
|
|
|
|
|
z: 1
|
|
|
|
|
|
renderTarget: Canvas.Image
|
|
|
|
|
|
|
|
|
|
|
|
onPaint: {
|
|
|
|
|
|
var ctx = getContext("2d")
|
|
|
|
|
|
ctx.clearRect(0, 0, width, height)
|
|
|
|
|
|
|
|
|
|
|
|
if (errorPosition === -1) return
|
|
|
|
|
|
|
|
|
|
|
|
// 获取错误位置在TextArea中的坐标
|
|
|
|
|
|
var rect = jsonEditor.positionToRectangle(errorPosition)
|
|
|
|
|
|
if (!rect || rect.width === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
// 关键步骤:映射到ScrollView的contentItem(Flickable)
|
|
|
|
|
|
var flickablePos = jsonEditor.mapToItem(textScrollView.contentItem, rect.x, rect.y)
|
|
|
|
|
|
|
|
|
|
|
|
// 计算滚动偏移后的可视坐标(减去滚动量)
|
|
|
|
|
|
var canvasX = flickablePos.x - textScrollView.contentItem.contentX
|
|
|
|
|
|
var canvasY = flickablePos.y + textScrollView.contentItem.contentY
|
|
|
|
|
|
|
|
|
|
|
|
// 调整标记到文本下方
|
|
|
|
|
|
canvasY += rect.height
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制红色三角形(向下箭头)
|
|
|
|
|
|
ctx.fillStyle = "#d13438"
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
|
ctx.moveTo(canvasX, canvasY) // 顶点
|
|
|
|
|
|
ctx.lineTo(canvasX - 8, canvasY + 10) // 左下
|
|
|
|
|
|
ctx.lineTo(canvasX + 8, canvasY + 10) // 右下
|
|
|
|
|
|
ctx.closePath()
|
|
|
|
|
|
ctx.fill()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动时实时更新
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: textScrollView.contentItem
|
|
|
|
|
|
function onContentYChanged(){
|
|
|
|
|
|
errorCanvas.requestPaint()
|
|
|
|
|
|
}
|
|
|
|
|
|
function onContentXChanged(){
|
|
|
|
|
|
errorCanvas.requestPaint()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按钮区域(底部固定高度)
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
|
anchors.top:header_area.bottom
|
|
|
|
|
|
anchors.topMargin: 6
|
|
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
|
|
spacing: 15
|
|
|
|
|
|
// 错误提示
|
|
|
|
|
|
Text {
|
|
|
|
|
|
id: errorLabel
|
|
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
|
|
visible: false
|
|
|
|
|
|
color: "#d13438"
|
|
|
|
|
|
font.bold: true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 09:46:04 +08:00
|
|
|
|
HusButton {
|
2025-05-29 14:04:05 +08:00
|
|
|
|
text: "格式化"
|
|
|
|
|
|
onClicked: {
|
|
|
|
|
|
jsonEditor.text = jsonProcessor.formatJson(jsonEditor.text)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 09:46:04 +08:00
|
|
|
|
HusButton {
|
2025-05-29 14:04:05 +08:00
|
|
|
|
text: "保存"
|
|
|
|
|
|
onClicked: saveFunction(jsonEditor.text)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|