初始提交

This commit is contained in:
2026-06-22 11:28:01 +08:00
commit 0a0d4fbd79
84 changed files with 8899 additions and 0 deletions

View File

@ -0,0 +1,213 @@
//
// HomeMoreFunctionsView.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import SwiftUI
///
struct HomeMoreFunctionsView: View {
@Environment(PermissionContext.self) private var permissionContext
@Environment(AppRouter.self) private var appRouter
@Environment(RouterPath.self) private var router
@Environment(\.dismiss) private var dismiss
@State private var viewModel = HomeViewModel()
@State private var commonUris: [String] = []
private let commonMenuStore = HomeCommonMenuStore()
private var commonItems: [HomeMenuItem] {
commonUris.compactMap(menuItem(for:))
}
private var moreItems: [HomeMenuItem] {
viewModel.menuItems.filter { item in
!isCommonURI(item.uri)
}
}
var body: some View {
VStack(spacing: 0) {
topBar
ScrollView {
VStack(alignment: .leading, spacing: 26) {
section(title: "常用应用", items: commonItems, isCommon: true)
section(title: "更多功能", items: moreItems, isCommon: false)
}
.padding(.horizontal, AppMetrics.Spacing.mediumLarge)
.padding(.top, AppMetrics.Spacing.xxLarge + 1)
.padding(.bottom, AppMetrics.Spacing.xxLarge)
}
.background(Color(hex: 0xF5F5F5))
}
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
.toolbar(.hidden, for: .navigationBar)
.task {
rebuildMenus()
}
.onChange(of: permissionContext.currentRole?.id) { _, _ in
rebuildMenus()
}
.onChange(of: permissionContext.rolePermissions.count) { _, _ in
rebuildMenus()
}
}
private var topBar: some View {
ZStack {
Text("全部功能")
.font(.system(size: AppMetrics.FontSize.title2 + 1, weight: .semibold))
.foregroundStyle(Color(hex: 0x2C2C2C))
.frame(maxWidth: .infinity)
HStack {
Button {
dismiss()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(Color(hex: 0x111827))
.frame(width: AppMetrics.ControlSize.iconTapArea, height: AppMetrics.ControlSize.iconTapArea)
}
.buttonStyle(.plain)
Spacer()
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
}
.frame(height: 62)
.background(.white)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.black.opacity(0.06))
.frame(height: 0.5)
}
}
private func section(title: String, items: [HomeMenuItem], isCommon: Bool) -> some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
Text(title)
.font(.system(size: AppMetrics.FontSize.title, weight: .bold))
.foregroundStyle(Color(hex: 0x2C2C2C))
appGrid(items: items, isCommon: isCommon)
}
}
private func appGrid(items: [HomeMenuItem], isCommon: Bool) -> some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 14), count: 3), spacing: AppMetrics.Spacing.mediumLarge) {
ForEach(items, id: \.uri) { item in
ZStack(alignment: .topTrailing) {
Button {
openMenu(item)
} label: {
VStack(spacing: AppMetrics.Spacing.mediumLarge + 1) {
menuIconView(for: item)
.frame(width: 34, height: 34)
Text(item.title)
.font(.system(size: AppMetrics.FontSize.callout))
.foregroundStyle(Color(hex: 0x252525))
.lineLimit(1)
.minimumScaleFactor(0.78)
}
.frame(maxWidth: .infinity)
.frame(height: 112)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
Button {
if isCommon {
commonUris = commonMenuStore.remove(item.uri, current: commonUris)
} else {
commonUris = commonMenuStore.add(item.uri, current: commonUris, menuItems: viewModel.menuItems)
}
} label: {
Image(systemName: isCommon ? "minus.circle.fill" : "plus.circle.fill")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(isCommon ? Color(hex: 0xFF1111) : AppDesign.primary)
.background(Color.white, in: Circle())
}
.buttonStyle(.plain)
.offset(x: 9, y: -9)
}
.frame(maxWidth: .infinity)
.frame(height: 112)
}
}
}
@ViewBuilder
private func menuIconView(for item: HomeMenuItem) -> some View {
if let src = item.iconSrc,
let url = URL(string: src),
!src.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image.resizable().scaledToFit()
default:
fallbackMenuIcon(for: item.uri)
}
}
} else {
fallbackMenuIcon(for: item.uri)
}
}
private func fallbackMenuIcon(for uri: String) -> some View {
Image(systemName: HomeIconCatalog.iconName(for: uri))
.resizable()
.scaledToFit()
.foregroundStyle(AppDesign.primary)
.frame(width: 34, height: 34)
}
private func menuItem(for uri: String) -> HomeMenuItem? {
let availableURIs = Set(viewModel.menuItems.map(\.uri))
let resolvedUri = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
guard let existing = viewModel.menuItems.first(where: { $0.uri == resolvedUri }) else {
return nil
}
return HomeMenuItem(
title: HomeMenuRouter.displayTitle(for: resolvedUri, fallback: existing.title),
uri: resolvedUri,
iconSrc: existing.iconSrc
)
}
private func isCommonURI(_ uri: String) -> Bool {
let aliasKey = HomeMenuRouter.menuAliasKey(for: uri)
return commonUris.contains { HomeMenuRouter.menuAliasKey(for: $0) == aliasKey }
}
private func openMenu(_ item: HomeMenuItem) {
switch HomeMenuRouter.resolve(uri: item.uri, title: item.title) {
case .tab(let tab):
appRouter.select(tab)
case .destination(let route):
if route == .moreFunctions {
return
}
router.navigate(to: .home(route))
case .unsupported(let uri, let title, _):
router.navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
case .placeholder(let uri, let title):
HomeRouteDiagnostics.recordUnknown(uri: uri, title: title)
router.navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
}
}
private func rebuildMenus() {
viewModel.buildMenus(
from: permissionContext.rolePermissions,
currentRoleId: permissionContext.currentRole?.id
)
commonUris = commonMenuStore.load(menuItems: viewModel.menuItems)
}
}