feat(ui): consolidate workspace add actions

This commit is contained in:
Qiu Yuzhou
2026-09-28 11:45:58 +08:00
parent 92040baef0
commit 63e33724f7
3 changed files with 97 additions and 57 deletions
+62 -7
View File
@@ -1,5 +1,23 @@
import SwiftUI
private enum WorkspaceSheet: String, Identifiable {
case importURL
case qrImport
case legacyImport
case addSubscription
var id: String { rawValue }
var destination: WorkspaceDestination {
switch self {
case .importURL, .qrImport, .legacyImport:
.servers
case .addSubscription:
.subscriptions
}
}
}
/// 主窗口外壳(地图 #52,票 #53):NavigationSplitView 侧栏承载五项导航与
/// 底部常驻代理状态卡;详情区按 route destination 承载各分区视图。分区大
/// 标题由窗口标题承担(组合根把 destination label 推给开窗器,见
@@ -20,8 +38,8 @@ struct MainWindowView: View {
let diagnosticReportExporter: any DiagnosticReportExporter
@State private var selection: NodeID?
/// 分区动作的呈现状态(票 #56/#58):添加订阅 sheet 与诊断导出由壳持有。
@State private var showAddSubscription = false
/// 全局添加菜单打开的表单,以及诊断导出由窗口壳持有。
@State private var presentedWorkspaceSheet: WorkspaceSheet?
@State private var exportedDiagnosticsPath: String?
@StateObject private var shellActionErrors = ErrorAlertPresenter()
@@ -34,12 +52,27 @@ struct MainWindowView: View {
}
.frame(minWidth: 920, minHeight: 580)
.toolbar {
ToolbarItem(placement: .navigation) {
addMenu
}
ToolbarItemGroup(placement: .primaryAction) {
destinationActions
}
}
.sheet(isPresented: $showAddSubscription) {
AddSubscriptionSheet(workflow: workflow, errors: shellActionErrors)
.toolbar(removing: .sidebarToggle)
.sheet(item: $presentedWorkspaceSheet) { sheet in
switch sheet {
case .importURL:
ImportURLSheet(
workflow: workflow, errors: shellActionErrors, clipboard: clipboard,
selection: $selection)
case .qrImport:
QRImportSheet(workflow: workflow, errors: shellActionErrors, selection: $selection)
case .legacyImport:
LegacyImportSheet(workflow: workflow)
case .addSubscription:
AddSubscriptionSheet(workflow: workflow, errors: shellActionErrors)
}
}
.alert(
"操作失败",
@@ -65,6 +98,31 @@ struct MainWindowView: View {
// MARK: - 分区动作槽位
private var addMenu: some View {
Menu {
Button("通过 URL 导入…") { presentWorkspaceSheet(.importURL) }
Button("从二维码图片导入…") { presentWorkspaceSheet(.qrImport) }
if workflow.legacyImportState.snapshotFound {
Button(legacyImportActionTitle) { presentWorkspaceSheet(.legacyImport) }
}
Divider()
Button("添加订阅…") { presentWorkspaceSheet(.addSubscription) }
} label: {
Label("添加", systemImage: "plus")
}
.labelStyle(.iconOnly)
.help("添加")
}
private var legacyImportActionTitle: String {
workflow.legacyImportState.completed ? "再次导入旧版本服务器…" : "导入旧版本服务器…"
}
private func presentWorkspaceSheet(_ sheet: WorkspaceSheet) {
route.navigate(to: sheet.destination)
presentedWorkspaceSheet = sheet
}
/// 分区动作(票 #53/#56/#57):页级动作随 destination 切换,经工具栏桥接
/// 出现在窗口工具栏右端;首页/服务器无页级动作。例外:设置的保存/恢复
/// 默认是表单级提交动作,由 SettingsView 内容顶部行承载(动作与表单同置,
@@ -79,9 +137,6 @@ struct MainWindowView: View {
ProgressView()
.controlSize(.small)
}
Button("添加订阅", systemImage: "plus") {
showAddSubscription = true
}
Button("更新全部", systemImage: "arrow.triangle.2.circlepath") {
Task { await workflow.refreshAllSubscriptions() }
}
+4 -4
View File
@@ -2,9 +2,9 @@ import SwiftUI
/// 订阅分区(spec #21 D11,issue #35/#41,地图 #52 票 #56):每订阅一张卡片
/// (图标 + 名称 + 「HTTPS 订阅地址已隐藏」+ 状态徽标 + 更新/重试 + 「…」菜单 +
/// 三列元数据 + 脱敏说明行),失败卡警告色描边。添加订阅与更新全部在窗口
/// 工具栏(由主窗口壳提供)。卡片数据来自订阅 projection(非敏感);结构化
/// 刷新状态在呈现层本地化(story 42)。
/// 三列元数据 + 脱敏说明行),失败卡警告色描边。添加订阅通过窗口工具栏全局
/// 「添加」菜单进入,更新全部仍在本页工具栏。卡片数据来自订阅 projection
/// (非敏感);结构化刷新状态在呈现层本地化(story 42)。
struct SubscriptionsView: View {
@ObservedObject var workflow: CatalogWorkflow
let errors: ErrorAlertPresenter
@@ -38,7 +38,7 @@ struct SubscriptionsView: View {
if workflow.subscriptions.isEmpty {
ContentUnavailableView(
"暂无订阅", systemImage: "arrow.triangle.2.circlepath",
description: Text("用工具栏「添加订阅」粘贴 HTTPS 订阅地址(SIP-008 JSON)")
description: Text("用窗口左上方的「添加」菜单添加 HTTPS 订阅地址(SIP-008 JSON)")
)
.allowsHitTesting(false)
}
+31 -46
View File
@@ -1,10 +1,10 @@
import SwiftUI
/// 服务器 destination(spec #21 D11,issue #32/#34/#35/#41):保留服务器目录树、
/// 详情编辑与导入行为。workspace destination 由外层主窗口壳持有(新壳在
/// MainWindowView.swift,票 #53);本视图只持有服务器功能的 sheet、alert、
/// 确认弹窗与编辑草稿边界;selection 由外层绑定,以便订阅删除仍能清除失效
/// 的服务器选择。
/// 服务器 destination(spec #21 D11,issue #32/#34/#35/#41):承载服务器目录树、
/// 详情编辑与分组命令。workspace destination 由外层主窗口壳持有(新壳在
/// MainWindowView.swift,票 #53);全局导入表单由窗口壳呈现,本视图持有服务器
/// 功能的 sheet、alert、确认弹窗与编辑草稿边界;selection 由外层绑定,以便订阅
/// 删除仍能清除失效的服务器选择。
struct ServersView: View {
@ObservedObject var workflow: CatalogWorkflow
@@ -21,11 +21,9 @@ struct ServersView: View {
@State private var renameText = ""
@State private var newGroupParent: NodeID?
@State private var newGroupName = ""
@State private var isPresentingNewGroup = false
@State private var deleteTarget: NodeID?
@State private var moveTarget: NodeID?
@State private var showImportURLSheet = false
@State private var showQRImportSheet = false
@State private var showLegacyImportSheet = false
@State private var rootDropHovering = false
var body: some View {
@@ -51,13 +49,11 @@ struct ServersView: View {
}
.alert(
"新建分组",
isPresented: Binding(
get: { newGroupParent != nil },
set: { if !$0 { newGroupParent = nil } })
isPresented: $isPresentingNewGroup
) {
TextField("名称", text: $newGroupName)
Button("创建") { commitNewGroup() }
Button("取消", role: .cancel) { newGroupParent = nil }
Button("取消", role: .cancel) { cancelNewGroup() }
}
.alert(
"操作失败",
@@ -81,16 +77,6 @@ struct ServersView: View {
} message: {
Text(deleteMessage)
}
.sheet(isPresented: $showImportURLSheet) {
ImportURLSheet(
workflow: workflow, errors: errors, clipboard: clipboard, selection: $selection)
}
.sheet(isPresented: $showQRImportSheet) {
QRImportSheet(workflow: workflow, errors: errors, selection: $selection)
}
.sheet(isPresented: $showLegacyImportSheet) {
LegacyImportSheet(workflow: workflow)
}
.sheet(
item: Binding(
get: { moveTarget.map(MoveContext.init) },
@@ -113,8 +99,7 @@ struct ServersView: View {
renameText = workflow.displayName(for: id)
},
onNewGroup: { parent in
newGroupParent = parent
newGroupName = ""
presentNewGroup(in: parent)
},
onMove: { moveTarget = $0 },
onDelete: { deleteTarget = $0 }
@@ -136,7 +121,7 @@ struct ServersView: View {
if workflow.tree.isEmpty {
ContentUnavailableView(
"暂无服务器", systemImage: "server.rack",
description: Text("用工具栏「添加」导入 ss:// 链接或新建分组")
description: Text("用工具栏的「添加」菜单导入 ss:// 链接,或用「新建分组」按钮创建分组")
)
.allowsHitTesting(false)
}
@@ -196,35 +181,34 @@ extension ServersView {
}
}
// MARK: - 工具栏(添加与新建分组)
// MARK: - 工具栏(新建分组)
@ToolbarContentBuilder
private var toolbarContent: some ToolbarContent {
ToolbarItem(placement: .primaryAction) {
Menu {
Button("通过 URL 导入…") { showImportURLSheet = true }
Button("从二维码图片导入…") { showQRImportSheet = true }
if workflow.legacyImportState.snapshotFound {
Divider()
Button(
workflow.legacyImportState.completed ? "再次导入 Legacy 配置…" : "导入 Legacy 配置…"
) {
showLegacyImportSheet = true
}
}
Divider()
Button("新建分组…") {
newGroupParent = workflow.importTargetParent(for: selection)
newGroupName = ""
}
Button {
presentNewGroup(in: workflow.importTargetParent(for: selection))
} label: {
Label("添加", systemImage: "plus")
Label("新建分组", systemImage: "folder.badge.plus")
}
.labelStyle(.iconOnly)
.help("新建分组")
}
}
// MARK: - 告警动作
private func presentNewGroup(in parent: NodeID?) {
newGroupParent = parent
newGroupName = ""
isPresentingNewGroup = true
}
private func cancelNewGroup() {
isPresentingNewGroup = false
newGroupParent = nil
}
private func commitRename() {
guard let id = renameTarget else { return }
renameTarget = nil
@@ -238,11 +222,12 @@ extension ServersView {
}
private func commitNewGroup() {
guard let parent = newGroupParent else { return }
newGroupParent = nil
let parent = newGroupParent
let name = newGroupName
cancelNewGroup()
Task {
do {
_ = try await workflow.createGroup(named: newGroupName, into: parent)
_ = try await workflow.createGroup(named: name, into: parent)
} catch {
errors.present(error)
}