import QtQuick import QtQuick.Controls.Material import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs import "." import TactileIPC 1.0 Rectangle { id: root width: 350 color: Backend.lightMode ? "#F5F5F5" : "#2C2C2C" radius: 8 border.color: Qt.rgba(0, 0, 0, 0.08) property color textColor: Backend.lightMode ? "#424242" : "#E0E0E0" function formatHexByte(value) { const hex = Number(value).toString(16).toUpperCase() return "0x" + ("00" + hex).slice(-2) } function formatHexValue(value) { const hex = Number(value).toString(16).toUpperCase() return "0x" + hex } function parseHexValue(text, maxValue) { let trimmed = String(text).trim() if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) trimmed = trimmed.slice(2) if (trimmed.length === 0) return NaN const value = parseInt(trimmed, 16) if (isNaN(value) || value < 0 || value > maxValue) return NaN return value } function tr(text) { I18n.retranslateToken return qsTr(text) } Material.theme: Backend.lightMode ? Material.Light : Material.Dark Material.accent: Material.Green Material.primary: Material.Green ScrollView { anchors.fill: parent ScrollBar.vertical.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ColumnLayout { id: layout anchors.fill: parent anchors.margins: 12 spacing: 12 CollapsiblePanel { title: root.tr("连接设置") expanded: true Layout.fillWidth: true RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("COM Port") Layout.preferredWidth: 90 color: root.textColor } ComboBox { id: portBox Layout.fillWidth: true model: Backend.serial.availablePorts Component.onCompleted: { for (let i = 0; i < portBox.count; i++) { if (portBox.textAt(i) === Backend.serial.portName) { currentIndex = i break } } } onActivated: Backend.serial.portName = currentText } ToolButton { text: "\u21bb" onClicked: Backend.serial.refreshPorts() } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("Baud") Layout.preferredWidth: 90 color: root.textColor } ComboBox { Layout.fillWidth: true model: ["9600", "57600", "115200", "230400", "921600"] Component.onCompleted: { const idx = model.indexOf(String(Backend.serial.baudRate)) if (idx >= 0) currentIndex = idx } onActivated: Backend.serial.baudRate = parseInt(currentText) } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("模式") Layout.preferredWidth: 90 color: root.textColor } ComboBox { Layout.fillWidth: true textRole: "text" model: [ { text: root.tr("从站"), value: "slave" }, { text: root.tr("主站"), value: "master" } ] function syncModeIndex() { for (let i = 0; i < model.length; i++) { if (model[i].value === Backend.serial.mode) { currentIndex = i break } } } Component.onCompleted: syncModeIndex() onModelChanged: syncModeIndex() Connections { target: Backend.serial function onModeChanged() { syncModeIndex() } } onActivated: Backend.serial.mode = model[currentIndex].value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("设备地址") Layout.preferredWidth: 90 color: root.textColor } TextField { id: addrField Layout.fillWidth: true text: root.formatHexByte(Backend.serial.deviceAddress) placeholderText: "0x01" inputMethodHints: Qt.ImhPreferUppercase validator: RegularExpressionValidator { regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,2}$/ } onEditingFinished: { const value = root.parseHexValue(text, 255) if (isNaN(value)) { text = root.formatHexByte(Backend.serial.deviceAddress) return } Backend.serial.deviceAddress = value text = root.formatHexByte(Backend.serial.deviceAddress) } Connections { target: Backend.serial function onDeviceAddressChanged() { addrField.text = root.formatHexByte(Backend.serial.deviceAddress) } } } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("采样周期") Layout.preferredWidth: 90 color: root.textColor } SpinBox { Layout.fillWidth: true from: 1 to: 2000 value: Backend.serial.pollIntervalMs enabled: Backend.serial.mode === "slave" onValueModified: Backend.serial.pollIntervalMs = value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("宽") Layout.preferredWidth: 90 color: root.textColor } SpinBox { Layout.fillWidth: true from: 1 to: 20 value: Backend.sensorCol enabled: Backend.serial.connected === false onValueModified: Backend.sensorCol = value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("高") Layout.preferredWidth: 90 color: root.textColor } SpinBox { Layout.fillWidth: true from: 1 to: 20 value: Backend.sensorRow enabled: Backend.serial.connected === false onValueModified: Backend.sensorRow = value } } RowLayout { Layout.fillWidth: true spacing: 12 Button { Layout.fillWidth: true text: root.tr("连接") highlighted: true enabled: !Backend.serial.connected onClicked: Backend.serial.open() } Button { Layout.fillWidth: true text: root.tr("断开") enabled: Backend.serial.connected onClicked: Backend.serial.close() } } } CollapsiblePanel { title: root.tr("采样参数") expanded: true Layout.fillWidth: true RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("功能码") Layout.preferredWidth: 90 color: root.textColor } SpinBox { id: requestFunctionBox Layout.fillWidth: true from: 0 to: 255 editable: true value: Backend.serial.requestFunction textFromValue: function(value, locale) { return root.formatHexByte(value) } valueFromText: function(text, locale) { const parsed = root.parseHexValue(text, requestFunctionBox.to) return isNaN(parsed) ? requestFunctionBox.value : parsed } validator: RegularExpressionValidator { regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,2}$/ } onValueModified: Backend.serial.requestFunction = value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("起始地址") Layout.preferredWidth: 90 color: root.textColor } SpinBox { id: requestStartAddressBox Layout.fillWidth: true from: 0 to: 1000000 editable: true value: Backend.serial.requestStartAddress textFromValue: function(value, locale) { return root.formatHexValue(value) } valueFromText: function(text, locale) { const parsed = root.parseHexValue(text, requestStartAddressBox.to) return isNaN(parsed) ? requestStartAddressBox.value : parsed } validator: RegularExpressionValidator { regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,8}$/ } onValueModified: Backend.serial.requestStartAddress = value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("读取长度") Layout.preferredWidth: 90 color: root.textColor } SpinBox { Layout.fillWidth: true from: 0 to: 65535 editable: true value: Backend.serial.requestLength onValueModified: Backend.serial.requestLength = value } } } CollapsiblePanel { title: root.tr("传感器规格") expanded: true Layout.fillWidth: true RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("协议") Layout.preferredWidth: 90 color: root.textColor } Label { text: Backend.serial.protocol Layout.fillWidth: true horizontalAlignment: Text.AlignRight color: root.textColor } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("型号") Layout.preferredWidth: 90 color: root.textColor } Label { text: Backend.serial.sensorModel Layout.fillWidth: true horizontalAlignment: Text.AlignRight color: root.textColor } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("规格") Layout.preferredWidth: 90 color: root.textColor } Label { text: Backend.serial.sensorGrid Layout.fillWidth: true horizontalAlignment: Text.AlignRight color: root.textColor } } Button { Layout.fillWidth: true text: root.tr("重新识别") highlighted: true } } CollapsiblePanel { title: root.tr("颜色映射") expanded: true Layout.fillWidth: true RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("最小值") Layout.preferredWidth: 90 color: root.textColor } SpinBox { Layout.fillWidth: true from: -999999 to: 999999 editable: true value: Backend.rangeMin onValueModified: Backend.rangeMin = value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("最大值") Layout.preferredWidth: 90 color: root.textColor } SpinBox { Layout.fillWidth: true from: -999999 to: 999999 editable: true value: Backend.rangeMax onValueModified: Backend.rangeMax = value } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("低色") Layout.preferredWidth: 90 color: root.textColor } Rectangle { width: 22 height: 22 radius: 4 color: Backend.colorLow border.width: 1 border.color: Qt.rgba(0, 0, 0, 0.2) Layout.alignment: Qt.AlignVCenter MouseArea { anchors.fill: parent onClicked: { lowColorDialog.selectedColor = Backend.colorLow lowColorDialog.open() } } } Button { text: root.tr("选择") Layout.fillWidth: true onClicked: { lowColorDialog.selectedColor = Backend.colorLow lowColorDialog.open() } } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("中色") Layout.preferredWidth: 90 color: root.textColor } Rectangle { width: 22 height: 22 radius: 4 color: Backend.colorMid border.width: 1 border.color: Qt.rgba(0, 0, 0, 0.2) Layout.alignment: Qt.AlignVCenter MouseArea { anchors.fill: parent onClicked: { midColorDialog.selectedColor = Backend.colorMid midColorDialog.open() } } } Button { text: root.tr("选择") Layout.fillWidth: true onClicked: { midColorDialog.selectedColor = Backend.colorMid midColorDialog.open() } } } RowLayout { Layout.fillWidth: true spacing: 8 Label { text: root.tr("高色") Layout.preferredWidth: 90 color: root.textColor } Rectangle { width: 22 height: 22 radius: 4 color: Backend.colorHigh border.width: 1 border.color: Qt.rgba(0, 0, 0, 0.2) Layout.alignment: Qt.AlignVCenter MouseArea { anchors.fill: parent onClicked: { highColorDialog.selectedColor = Backend.colorHigh highColorDialog.open() } } } Button { text: root.tr("选择") Layout.fillWidth: true onClicked: { highColorDialog.selectedColor = Backend.colorHigh highColorDialog.open() } } } } CollapsiblePanel { title: root.tr("显示控制") expanded: true Layout.fillWidth: true CheckBox { text: root.tr("显示网络") checked: Backend.showGrid onToggled: Backend.showGrid = checked } CheckBox { text: root.tr("显示坐标轴") checked: false } RowLayout { Layout.fillWidth: true spacing: 12 Button { Layout.fillWidth: true text: root.tr("回放数据") highlighted: true } Button { Layout.fillWidth: true text: root.tr("导出数据") onClicked: { if (Backend.data.frameCount != 0) { exportDlg.open() } else { console.log("Backend.data.frameCount() === 0") } } } } } Item { Layout.fillHeight: true } } } ColorDialog { id: lowColorDialog title: root.tr("选择低色") onAccepted: Backend.colorLow = selectedColor } ColorDialog { id: midColorDialog title: root.tr("选择中色") onAccepted: Backend.colorMid = selectedColor } ColorDialog { id: highColorDialog title: root.tr("选择高色") onAccepted: Backend.colorHigh = selectedColor } SaveAsExportDialog { id: exportDlg /* onSaveTo: (folder, filename, format, method) => { console.log("保存目录:", folder) console.log("文件名:", filename) console.log("格式:", format, "方式:", method) } */ onSaveTo: (folder, filename, format, method) => { Backend.data.exportHandler(folder, filename, format, method) } } }