Skip to content

Commit b0173e9

Browse files
committed
create View and Github Actions
1 parent 9077d7d commit b0173e9

28 files changed

Lines changed: 1897 additions & 0 deletions

.github/workflows/build.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Build iOS App via XcodeGen
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
runs-on: macos-15
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Install XcodeGen
17+
run: brew install xcodegen
18+
19+
- name: Generate Xcode Project
20+
run: xcodegen generate
21+
22+
- name: Build Archive (.xcarchive)
23+
run: |
24+
xcodebuild -scheme MyTesla \
25+
-configuration Release \
26+
-archivePath MyTesla.xcarchive \
27+
CODE_SIGN_IDENTITY="" \
28+
CODE_SIGNING_ALLOWED="NO" \
29+
archive
30+
31+
- name: Export .ipa (Unsigned)
32+
run: |
33+
xcodebuild -exportArchive \
34+
-archivePath MyTesla.xcarchive \
35+
-exportPath . \
36+
-exportOptionsPlist ExportOptions.plist \
37+
CODE_SIGNING_ALLOWED="NO"
38+
39+
- name: Upload IPA Artifact
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: MyTesla-unsigned
43+
path: MyTesla.ipa

ExportOptions.plist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>method</key>
6+
<string>development</string>
7+
<key>teamID</key>
8+
<string>$(DEVELOPMENT_TEAM)</string>
9+
<key>signingStyle</key>
10+
<string>automatic</string>
11+
</dict>
12+
</plist>

MyTesla/Info.plist

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>zh-Hans</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>MyTesla</string>
9+
<key>CFBundleExecutable</key>
10+
<string>$(EXECUTABLE_NAME)</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>com.yourcompany.mytesla</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>MyTesla</string>
17+
<key>CFBundlePackageType</key>
18+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>1.0</string>
21+
<key>CFBundleVersion</key>
22+
<string>1</string>
23+
<key>LSRequiresIPhoneOS</key>
24+
<true/>
25+
<key>NSAppTransportSecurity</key>
26+
<dict>
27+
<key>NSAllowsArbitraryLoads</key>
28+
<true/>
29+
<key>NSAllowsLocalNetworking</key>
30+
<true/>
31+
</dict>
32+
<key>NSLocationWhenInUseUsageDescription</key>
33+
<string>需要您的定位权限以在地图上显示车辆位置和轨迹</string>
34+
<key>UIApplicationSceneManifest</key>
35+
<dict>
36+
<key>UIApplicationSupportsMultipleScenes</key>
37+
<false/>
38+
</dict>
39+
<key>UILaunchScreen</key>
40+
<dict/>
41+
<key>UIRequiredDeviceCapabilities</key>
42+
<array>
43+
<string>arm64</string>
44+
</array>
45+
<key>UISupportedInterfaceOrientations</key>
46+
<array>
47+
<string>UIInterfaceOrientationPortrait</string>
48+
</array>
49+
</dict>
50+
</plist>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// ErrorView.swift
3+
// MyTesla
4+
//
5+
6+
import SwiftUI
7+
8+
struct ErrorView: View {
9+
let message: String
10+
let retryAction: () -> Void
11+
12+
var body: some View {
13+
VStack(spacing: 16) {
14+
Image(systemName: "exclamationmark.triangle")
15+
.font(.largeTitle)
16+
.foregroundColor(.orange)
17+
Text(message)
18+
.font(.subheadline)
19+
.foregroundColor(.secondary)
20+
.multilineTextAlignment(.center)
21+
Button("重试") {
22+
retryAction()
23+
}
24+
.buttonStyle(.borderedProminent)
25+
}
26+
.padding()
27+
.frame(maxWidth: .infinity, maxHeight: .infinity)
28+
.padding(.top, 80)
29+
}
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// LoadingView.swift
3+
// MyTesla
4+
//
5+
6+
import SwiftUI
7+
8+
struct LoadingView: View {
9+
var body: some View {
10+
VStack(spacing: 16) {
11+
ProgressView()
12+
.scaleEffect(1.2)
13+
Text("加载中...")
14+
.font(.caption)
15+
.foregroundColor(.secondary)
16+
}
17+
.frame(maxWidth: .infinity, maxHeight: .infinity)
18+
.padding(.top, 80)
19+
}
20+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// BatteryCard.swift
3+
// MyTesla
4+
//
5+
6+
import SwiftUI
7+
8+
struct BatteryCard: View {
9+
let status: VehicleStatus
10+
11+
var body: some View {
12+
HStack {
13+
ZStack {
14+
Circle()
15+
.stroke(Color(.systemGray4), lineWidth: 8)
16+
.frame(width: 80, height: 80)
17+
if status.batteryLevel > 0 {
18+
Circle()
19+
.trim(from: 0, to: CGFloat(status.batteryLevel) / 100)
20+
.stroke(batteryColor, style: StrokeStyle(lineWidth: 8, lineCap: .round))
21+
.frame(width: 80, height: 80)
22+
.rotationEffect(.degrees(-90))
23+
.animation(.easeInOut, value: status.batteryLevel)
24+
}
25+
Text("\(status.batteryLevel)%")
26+
.font(.title2)
27+
.fontWeight(.bold)
28+
.monospacedDigit()
29+
}
30+
VStack(alignment: .leading, spacing: 4) {
31+
Text("续航里程")
32+
.font(.subheadline)
33+
.foregroundColor(.secondary)
34+
Text("\(Int(status.range)) km")
35+
.font(.title2)
36+
.fontWeight(.semibold)
37+
.monospacedDigit()
38+
if let odometer = status.odometer {
39+
Text("总里程 \(Int(odometer)) km")
40+
.font(.caption)
41+
.foregroundColor(.secondary)
42+
}
43+
}
44+
.padding(.leading, 8)
45+
Spacer()
46+
}
47+
.padding()
48+
.background(.regularMaterial)
49+
.clipShape(RoundedRectangle(cornerRadius: 16))
50+
}
51+
52+
private var batteryColor: Color {
53+
let level = status.batteryLevel
54+
if level > 50 { return .green }
55+
if level > 20 { return .orange }
56+
return .red
57+
}
58+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// ChargingCard.swift
3+
// MyTesla
4+
//
5+
6+
import SwiftUI
7+
8+
struct ChargingCard: View {
9+
let status: VehicleStatus
10+
11+
var body: some View {
12+
VStack(alignment: .leading, spacing: 8) {
13+
HStack {
14+
Image(systemName: "bolt.circle.fill")
15+
.foregroundColor(.green)
16+
Text("充电中")
17+
.font(.headline)
18+
Spacer()
19+
if let power = status.chargerPower {
20+
Text("\(String(format: "%.1f", power)) kW")
21+
.font(.headline)
22+
.monospacedDigit()
23+
} else {
24+
Text("功率未知")
25+
.font(.headline)
26+
.foregroundColor(.secondary)
27+
}
28+
}
29+
Divider()
30+
HStack {
31+
VStack(alignment: .leading) {
32+
Text("当前电量")
33+
.font(.caption)
34+
.foregroundColor(.secondary)
35+
Text("\(status.batteryLevel)%")
36+
.font(.title3)
37+
.monospacedDigit()
38+
}
39+
Spacer()
40+
if let remaining = status.chargeTimeRemaining {
41+
VStack(alignment: .trailing) {
42+
Text("预计充满")
43+
.font(.caption)
44+
.foregroundColor(.secondary)
45+
Text("\(remaining)分钟")
46+
.font(.title3)
47+
.monospacedDigit()
48+
}
49+
} else {
50+
VStack(alignment: .trailing) {
51+
Text("预计充满")
52+
.font(.caption)
53+
.foregroundColor(.secondary)
54+
Text("计算中...")
55+
.font(.title3)
56+
.foregroundColor(.secondary)
57+
}
58+
}
59+
}
60+
if status.batteryLevel < 100 {
61+
ProgressView(value: Double(status.batteryLevel), total: 100)
62+
.progressViewStyle(.linear)
63+
.tint(.green)
64+
.padding(.top, 4)
65+
} else {
66+
HStack {
67+
Image(systemName: "checkmark.circle.fill")
68+
.foregroundColor(.green)
69+
Text("已充满")
70+
.font(.subheadline)
71+
.foregroundColor(.green)
72+
}
73+
.padding(.top, 4)
74+
}
75+
}
76+
.padding()
77+
.background(.regularMaterial)
78+
.clipShape(RoundedRectangle(cornerRadius: 16))
79+
}
80+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//
2+
// DashboardView.swift
3+
// MyTesla
4+
//
5+
6+
import SwiftUI
7+
import SwiftData
8+
9+
struct DashboardView: View {
10+
@StateObject private var viewModel = DashboardViewModel()
11+
@Environment(\.modelContext) private var modelContext
12+
@State private var vehicleId: Int?
13+
14+
var body: some View {
15+
NavigationStack {
16+
ScrollView {
17+
VStack(spacing: 12) {
18+
if viewModel.isLoading && viewModel.status == nil {
19+
LoadingView()
20+
} else if let error = viewModel.errorMessage {
21+
ErrorView(message: error) {
22+
Task { await viewModel.refresh() }
23+
}
24+
} else {
25+
ForEach(viewModel.cardOrder, id: \.self) { cardType in
26+
switch cardType {
27+
case .vehicleInfo:
28+
if let vehicle = viewModel.vehicleDetail {
29+
VehicleInfoCard(vehicle: vehicle)
30+
}
31+
case .battery:
32+
if let status = viewModel.status {
33+
BatteryCard(status: status)
34+
}
35+
case .charging:
36+
if let status = viewModel.status, status.isCharging {
37+
ChargingCard(status: status)
38+
}
39+
case .drivingSummary:
40+
if let drive = viewModel.recentDrive {
41+
DrivingSummaryCard(drive: drive)
42+
}
43+
case .statusGrid:
44+
if let status = viewModel.status {
45+
StatusGridCard(status: status)
46+
}
47+
case .location:
48+
if let status = viewModel.status {
49+
LocationCard(status: status)
50+
}
51+
}
52+
}
53+
}
54+
}
55+
.padding(.horizontal, 16)
56+
.padding(.top, 8)
57+
}
58+
.background(Color(.systemBackground))
59+
.refreshable {
60+
await viewModel.refresh()
61+
}
62+
.navigationTitle("MyTesla")
63+
.navigationBarTitleDisplayMode(.large)
64+
.toolbar {
65+
ToolbarItem(placement: .topBarTrailing) {
66+
if let lastUpdated = viewModel.lastUpdated {
67+
Text(lastUpdated.timeAgoDisplay())
68+
.font(.caption)
69+
.foregroundColor(.secondary)
70+
}
71+
}
72+
}
73+
.task {
74+
await loadVehicleAndRefresh()
75+
}
76+
}
77+
}
78+
79+
private func loadVehicleAndRefresh() async {
80+
do {
81+
let vehicles = try await APIClient.shared.getVehicles()
82+
if let first = vehicles.first {
83+
vehicleId = first.id
84+
viewModel.configure(context: modelContext, vehicleId: first.id)
85+
await viewModel.refresh()
86+
} else {
87+
viewModel.errorMessage = "未找到车辆,请检查 TeslaMate 配置"
88+
}
89+
} catch {
90+
viewModel.errorMessage = error.localizedDescription
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)