Files
tactileipc3d/qml/content/SaveAsExportDialog.qml

562 lines
22 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}