feat: 支持构建时注入飞书配置并优化设置页面体验

This commit is contained in:
tigerenwork 2026-07-05 22:57:36 +08:00
parent 5e4587909d
commit 4d67d8bffd
4 changed files with 80 additions and 25 deletions

View File

@ -4,3 +4,8 @@
FEISHU_APP_ID = cli_your_app_id FEISHU_APP_ID = cli_your_app_id
FEISHU_APP_SECRET = your_app_secret FEISHU_APP_SECRET = your_app_secret
FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn
// Bitable app token & table ID (from the Bitable URL). Baked into the app at
// build time as defaults so internal users don't have to enter them in Settings.
FEISHU_APP_TOKEN = your_bitable_app_token
FEISHU_TABLE_ID = your_bitable_table_id

View File

@ -28,5 +28,9 @@
<string>$(FEISHU_APP_SECRET)</string> <string>$(FEISHU_APP_SECRET)</string>
<key>FEISHU_BASE_DOMAIN</key> <key>FEISHU_BASE_DOMAIN</key>
<string>$(FEISHU_BASE_DOMAIN)</string> <string>$(FEISHU_BASE_DOMAIN)</string>
<key>FEISHU_APP_TOKEN</key>
<string>$(FEISHU_APP_TOKEN)</string>
<key>FEISHU_TABLE_ID</key>
<string>$(FEISHU_TABLE_ID)</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,8 +1,8 @@
import Foundation import Foundation
struct AppConfig: Codable, Equatable { struct AppConfig: Codable, Equatable {
var appToken: String = "" var appToken: String = BundledDefault.appToken
var tableId: String = "" var tableId: String = BundledDefault.tableId
var assigneeName: String = "" var assigneeName: String = ""
var fieldMappings: FieldMappings = FieldMappings() var fieldMappings: FieldMappings = FieldMappings()
var pollIntervalSeconds: Int = 300 var pollIntervalSeconds: Int = 300
@ -11,7 +11,7 @@ struct AppConfig: Codable, Equatable {
var dailyRefreshTimes: [String] = [] var dailyRefreshTimes: [String] = []
var showFloatingWidget: Bool = false var showFloatingWidget: Bool = false
var launchAtLogin: Bool = true var launchAtLogin: Bool = true
var feishuBaseDomain: String = "xorbitlab.feishu.cn" var feishuBaseDomain: String = BundledDefault.feishuBaseDomain
/// When false (the default), Bugger waits for the next scheduled poll /// When false (the default), Bugger waits for the next scheduled poll
/// instead of fetching immediately on launch. Avoids a crash-on-start /// instead of fetching immediately on launch. Avoids a crash-on-start
/// loop if the table fetch is failing. /// loop if the table fetch is failing.
@ -66,15 +66,31 @@ struct AppConfig: Codable, Equatable {
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? "" appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? BundledDefault.appToken
tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? "" tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? BundledDefault.tableId
assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? "" assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? ""
fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings() fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings()
pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300 pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300
dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? [] dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? []
showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false
launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true
feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? "xorbitlab.feishu.cn" feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? BundledDefault.feishuBaseDomain
refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false
} }
} }
/// Values baked into the app at build time via Config.xcconfig Info.plist.
/// Used as defaults so internal users don't have to enter them in Settings.
enum BundledDefault {
static var appToken: String { string(for: "FEISHU_APP_TOKEN") }
static var tableId: String { string(for: "FEISHU_TABLE_ID") }
static var feishuBaseDomain: String { string(for: "FEISHU_BASE_DOMAIN") }
private static func string(for key: String) -> String {
let raw = (Bundle.main.object(forInfoDictionaryKey: key) as? String) ?? ""
// Xcode leaves an undefined $(...) substitution as the literal string;
// treat that as "not set" rather than showing "$(FEISHU_APP_TOKEN)".
if raw.hasPrefix("$(") { return "" }
return raw.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View File

@ -13,11 +13,29 @@ struct SettingsView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 12) {
// Highlighted "Start here" card lives outside the Form so its
// Spacer + multi-element HStack don't disturb the Form's
// two-column label/control layout (which was pushing the
// Feishu Bitable fields off the right edge of the window).
HStack(spacing: 12) {
Image(systemName: "person.fill")
.font(.title2)
.foregroundStyle(.tint)
Text("Your Name")
.font(.headline)
TextField("e.g. Alice", text: $config.assigneeName)
.textFieldStyle(.roundedBorder)
Spacer()
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 8).fill(.tint.opacity(0.08)))
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(.tint.opacity(0.35), lineWidth: 1))
Form { Form {
Section("Feishu Bitable") { Section("Feishu Bitable") {
TextField("App Token (from Bitable URL)", text: $config.appToken) TextField("App Token (from Bitable URL)", text: $config.appToken)
TextField("Table ID", text: $config.tableId) TextField("Table ID", text: $config.tableId)
TextField("Your Name (as in Assignee column, optional)", text: $config.assigneeName)
TextField("Feishu domain", text: $config.feishuBaseDomain) TextField("Feishu domain", text: $config.feishuBaseDomain)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
@ -107,7 +125,13 @@ struct SettingsView: View {
Toggle("Launch at login", isOn: $config.launchAtLogin) Toggle("Launch at login", isOn: $config.launchAtLogin)
} }
Section("Field Mappings") { Section {
DisclosureGroup(isExpanded: $showAdvanced) {
Text("Field Mappings")
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
TextField("Title field", text: $config.fieldMappings.titleField) TextField("Title field", text: $config.fieldMappings.titleField)
TextField("Priority field", text: $config.fieldMappings.priorityField) TextField("Priority field", text: $config.fieldMappings.priorityField)
TextField("Status field", text: $config.fieldMappings.statusField) TextField("Status field", text: $config.fieldMappings.statusField)
@ -116,9 +140,11 @@ struct SettingsView: View {
TextField("Customer field", text: $config.fieldMappings.customerField) TextField("Customer field", text: $config.fieldMappings.customerField)
TextField("Created At field", text: $config.fieldMappings.createdAtField) TextField("Created At field", text: $config.fieldMappings.createdAtField)
TextField("Updated At field", text: $config.fieldMappings.updatedAtField) TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
}
Section("Status Mappings") { Text("Status Mappings")
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
Text("Map your Feishu status values to standard Bugger statuses.") Text("Map your Feishu status values to standard Bugger statuses.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -126,6 +152,9 @@ struct SettingsView: View {
ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
statusMappingRow(for: status) statusMappingRow(for: status)
} }
} label: {
Label("Advanced: field & status mappings", systemImage: "slider.horizontal.3")
}
} }
HStack { HStack {
@ -140,7 +169,8 @@ struct SettingsView: View {
} }
.foregroundStyle(.red) .foregroundStyle(.red)
} }
} } // Form
} // VStack
.padding() .padding()
} }
.frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800) .frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)