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