完成主要交互、高性能组件、国际化和A型传感器数据包接收

This commit is contained in:
2026-01-13 16:34:28 +08:00
parent 47e6dc7244
commit 1960e6a5b9
84 changed files with 7752 additions and 332 deletions

View File

@@ -0,0 +1,561 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import Qt.labs.folderlistmodel 2.15
import QtCore 6.2
import QtQuick.Dialogs
import TactileIPC 1.0
Window {
id: root
width: 980
height: 640
minimumWidth: 880
minimumHeight: 560
visible: false
modality: Qt.ApplicationModal
flags: Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
title: root.tr("导出数据")
color: windowBg
readonly property bool isDark: !Backend.lightMode
readonly property color windowBg: isDark ? "#1B1F1B" : "#F7F8F9"
Material.accent: root.accent
Material.primary: root.accent
Material.theme: root.isDark ? Material.Dark : Material.Light
readonly property color accent: "#21A453"
readonly property color accentSoft: root.isDark ? "#1F3A2A" : "#E6F6EC"
readonly property color panel: root.isDark ? "#242924" : "#FFFFFF"
readonly property color border: root.isDark ? "#343A35" : "#E1E5EA"
readonly property color text: root.isDark ? "#E6ECE7" : "#1E2A32"
readonly property color subText: root.isDark ? "#9AA5A0" : "#6E7A86"
readonly property color fieldBg: root.isDark ? "#1E221E" : "#FFFFFF"
readonly property color surfaceAlt: root.isDark ? "#202520" : "#F9FAFB"
readonly property color hoverBg: root.isDark ? "#2C322D" : "#F3F6F8"
readonly property color iconBg: root.isDark ? "#25362B" : "#E8F3EA"
readonly property color iconBgAlt: root.isDark ? "#2A302A" : "#EFF2F5"
readonly property color disabledBg: root.isDark ? "#4B544E" : "#C9D2D8"
readonly property string uiFont: "Microsoft YaHei UI"
property url currentFolder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/"
property string chosenFilename: ""
property string exportFormat: formatBox.currentValue
property string exportMethod: methodBox.currentValue
property string lastMethodValue: "overwrite"
signal saveTo(url folder, string filename, string format, string method)
function open() {
centerOnScreen_()
visible = true
requestActivate()
}
function accept() {
visible = false
}
function reject() {
visible = false
}
function centerOnScreen_() {
x = Math.round((Screen.width - width) / 2)
y = Math.round((Screen.height - height) / 2)
}
function normalizeFolder_(path) {
if (!path)
return path
if (path.endsWith("/"))
return path
return path + "/"
}
function tr(text) {
I18n.retranslateToken
return qsTr(text)
}
onVisibleChanged: if (visible) centerOnScreen_()
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Rectangle {
Layout.fillWidth: true
height: 54
radius: 6
color: root.panel
border.color: root.border
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
ToolButton {
id: backBtn
text: "<"
font.family: root.uiFont
onClicked: nav.back()
background: Rectangle {
radius: 4
color: backBtn.hovered ? root.accentSoft : "transparent"
border.color: backBtn.hovered ? root.accent : root.border
}
}
ToolButton {
id: forwardBtn
text: ">"
font.family: root.uiFont
onClicked: nav.forward()
background: Rectangle {
radius: 4
color: forwardBtn.hovered ? root.accentSoft : "transparent"
border.color: forwardBtn.hovered ? root.accent : root.border
}
}
ToolButton {
id: upBtn
text: "^"
font.family: root.uiFont
onClicked: nav.up()
background: Rectangle {
radius: 4
color: upBtn.hovered ? root.accentSoft : "transparent"
border.color: upBtn.hovered ? root.accent : root.border
}
}
TextField {
id: breadcrumb
Layout.fillWidth: true
readOnly: true
font.family: root.uiFont
color: root.text
text: root.currentFolder.toString()
background: Rectangle {
radius: 4
color: root.surfaceAlt
border.color: root.border
}
}
TextField {
id: searchField
Layout.preferredWidth: 220
placeholderText: root.tr("在此位置中搜索")
font.family: root.uiFont
background: Rectangle {
radius: 4
color: root.fieldBg
border.color: root.border
}
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 12
Rectangle {
Layout.preferredWidth: 220
Layout.fillHeight: true
radius: 6
color: root.panel
border.color: root.border
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
Label {
text: root.tr("位置")
font.bold: true
font.family: root.uiFont
color: root.text
}
ListView {
id: places
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: [
{ name: root.tr("此电脑"), url: "file:///", icon: root.isDark ? "qrc:/images/computer_dark.png" : "qrc:/images/computer_light.png" },
{ name: root.tr("桌面"), url: StandardPaths.writableLocation(StandardPaths.DesktopLocation) + "/", icon: root.isDark ? "qrc:/images/desktop_dark.png" : "qrc:/images/desktop_light.png" },
{ name: root.tr("文档"), url: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/", icon: root.isDark ? "qrc:/images/docs_dark.png" : "qrc:/images/docs_light.png" },
{ name: root.tr("下载"), url: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + "/", icon: root.isDark ? "qrc:/images/download_dark.png" : "qrc:/images/download_light.png" }
]
delegate: ItemDelegate {
width: ListView.view.width
onClicked: {
places.currentIndex = index
root.currentFolder = normalizeFolder_(modelData.url)
}
background: Rectangle {
radius: 4
color: places.currentIndex === index ? root.accentSoft : "transparent"
border.color: places.currentIndex === index ? root.accent : "transparent"
}
contentItem: RowLayout {
spacing: 8
Image {
width: 16
height: 16
source: modelData.icon
fillMode: Image.PreserveAspectFit
smooth: true
}
Label {
text: modelData.name
font.family: root.uiFont
color: root.text
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 6
color: root.panel
border.color: root.border
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 6
RowLayout {
Layout.fillWidth: true
Label { text: root.tr("名称"); Layout.fillWidth: true; font.bold: true; font.family: root.uiFont; color: root.text }
Label { text: root.tr("修改时间"); Layout.preferredWidth: 160; font.bold: true; font.family: root.uiFont; color: root.text }
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.border }
FolderListModel {
id: fileModel
folder: root.currentFolder
showDotAndDotDot: false
showDirs: true
showFiles: false
sortField: FolderListModel.Name
nameFilters: searchField.text.trim().length > 0
? ["*" + searchField.text.trim() + "*"]
: ["*"]
}
ListView {
id: fileList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: fileModel
delegate: ItemDelegate {
id: fileRow
width: ListView.view.width
onDoubleClicked: {
root.currentFolder = normalizeFolder_(fileModel.get(index, "filePath"))
}
onClicked: {
fileList.currentIndex = index
}
background: Rectangle {
radius: 4
color: fileRow.hovered ? root.hoverBg : "transparent"
}
contentItem: RowLayout {
spacing: 8
Rectangle {
width: 18
height: 18
radius: 3
color: root.iconBg
border.color: root.border
// TODO: replace with folder icons.
// Text {
// anchors.centerIn: parent
// text: "DIR"
// font.pixelSize: 10
// color: root.subText
// }
Image {
// anchors.centerIn: parent
width: 16
height: 16
source: root.isDark ? "qrc:/images/folder_dark.png" : "qrc:/images/folder_light.png"
fillMode: Image.PreserveAspectFit
smooth: true
}
}
Label {
Layout.fillWidth: true
elide: Text.ElideRight
text: fileModel.get(index, "fileName") || ""
font.family: root.uiFont
color: root.text
}
Label {
Layout.preferredWidth: 160
text: {
const d = fileModel.get(index, "modified")
return d ? Qt.formatDateTime(d, "yyyy/MM/dd hh:mm") : ""
}
font.family: root.uiFont
color: root.subText
}
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: footerLayout.implicitHeight + 20
Layout.preferredHeight: implicitHeight
Layout.minimumHeight: implicitHeight
radius: 6
color: root.panel
border.color: root.border
GridLayout {
id: footerLayout
anchors.fill: parent
anchors.margins: 10
columns: 6
columnSpacing: 10
rowSpacing: 8
Label { text: root.tr("文件名"); font.family: root.uiFont; color: root.text }
TextField {
id: filenameField
Layout.columnSpan: 3
Layout.fillWidth: true
placeholderText: root.tr("输入要保存的文件名")
font.family: root.uiFont
background: Rectangle {
radius: 4
color: root.fieldBg
border.color: root.border
}
}
Label { text: root.tr("文件类型"); font.family: root.uiFont; color: root.text }
ComboBox {
id: formatBox
Layout.fillWidth: true
implicitHeight: 32
Layout.preferredHeight: implicitHeight
textRole: "label"
valueRole: "value"
font.family: root.uiFont
topPadding: 6
bottomPadding: 6
contentItem: Item {
anchors.fill: parent
Text {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 24
text: formatBox.displayText
font: formatBox.font
color: root.text
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
model: [
{ label: "CSV (*.csv)", value: "csv", suffix: ".csv" },
{ label: "JSON (*.json)", value: "json", suffix: ".json" },
{ label: "Excel (*.xlsx)", value: "xlsx", suffix: ".xlsx" }
]
background: Rectangle {
radius: 4
color: root.fieldBg
border.color: root.border
}
}
Label { text: root.tr("导出方式"); font.family: root.uiFont; color: root.text }
ComboBox {
id: methodBox
Layout.columnSpan: 5
Layout.fillWidth: true
implicitHeight: 32
Layout.preferredHeight: implicitHeight
textRole: "label"
valueRole: "value"
font.family: root.uiFont
topPadding: 6
bottomPadding: 6
contentItem: Item {
anchors.fill: parent
Text {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 24
text: methodBox.displayText
font: methodBox.font
color: root.text
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
model: [
{ label: root.tr("覆盖导出(同名替换)"), value: "overwrite" },
{ label: root.tr("追加导出(写入同一文件)"), value: "append" },
{ label: root.tr("压缩导出zip"), value: "zip" }
]
function syncMethodIndex() {
for (let i = 0; i < model.length; i++) {
if (model[i].value === root.lastMethodValue) {
currentIndex = i
break
}
}
}
Component.onCompleted: syncMethodIndex()
onModelChanged: syncMethodIndex()
onCurrentValueChanged: root.lastMethodValue = currentValue
background: Rectangle {
radius: 4
color: root.fieldBg
border.color: root.border
}
}
Item { Layout.columnSpan: 4; Layout.fillWidth: true }
RowLayout {
Layout.columnSpan: 2
Layout.alignment: Qt.AlignRight
spacing: 10
Button {
id: cancelBtn
text: root.tr("取消")
font.family: root.uiFont
background: Rectangle {
radius: 4
color: "transparent"
border.color: root.border
}
contentItem: Text {
text: cancelBtn.text
font.family: root.uiFont
color: root.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: root.reject()
}
Button {
id: saveBtn
text: root.tr("保存")
enabled: filenameField.text.trim().length > 0
font.family: root.uiFont
background: Rectangle {
radius: 4
color: saveBtn.enabled ? root.accent : root.disabledBg
border.color: saveBtn.enabled ? root.accent : root.disabledBg
}
contentItem: Text {
text: saveBtn.text
font.family: root.uiFont
color: "#FFFFFF"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: doSave()
}
}
}
}
}
MessageDialog {
id: overwriteDlg
title: root.tr("文件已存在")
text: root.tr("目标文件已存在,是否覆盖?")
buttons: MessageDialog.Yes | MessageDialog.No
onButtonClicked: function (button, role) {
switch (button) {
case MessageDialog.Yes:
finalizeSave(true)
break
case MessageDialog.No:
break
}
}
}
function finalizeSave(forceOverwrite) {
root.saveTo(root.currentFolder, chosenFilename, exportFormat, exportMethod)
root.accept()
}
function doSave() {
let name = filenameField.text.trim()
if (name.length === 0) return
const suffix = formatBox.model[formatBox.currentIndex].suffix
if (!name.endsWith(suffix)) name += suffix
chosenFilename = name
// TODO: check file existence and show overwriteDlg when needed.
finalizeSave(false)
}
QtObject {
id: nav
property var backStack: []
property var forwardStack: []
function push(url) {
backStack.push(url)
forwardStack = []
}
function back() {
if (backStack.length === 0) return
forwardStack.push(root.currentFolder)
root.currentFolder = backStack.pop()
}
function forward() {
if (forwardStack.length === 0) return
backStack.push(root.currentFolder)
root.currentFolder = forwardStack.pop()
}
function up() {
let s = root.currentFolder.toString()
if (s.endsWith("/")) s = s.slice(0, -1)
const idx = s.lastIndexOf("/")
if (idx > 8) root.currentFolder = s.slice(0, idx + 1)
}
}
onCurrentFolderChanged: {
if (breadcrumb.text && breadcrumb.text !== root.currentFolder.toString())
nav.push(breadcrumb.text)
breadcrumb.text = root.currentFolder.toString()
}
}