feat: 全平台打包 + CI/CD 自动构建工作流
- 修复移动端: axios替换为原生fetch (React Native兼容) - 新增 .gitea/workflows/build.yml CI/CD工作流: - Windows: NSIS安装包 (windows-latest) - macOS: DMG x64+arm64 (macos-latest) - Linux: AppImage+deb (ubuntu-latest) - Android: APK via expo prebuild + gradle (ubuntu-latest) - iOS: simulator build (macos-latest) - 移动端JS Bundle导出 (android+ios) - 自动创建Release (tag触发) 本地已构建产物: - client/release/FunConnect-1.1.0-Win-x64.exe (73MB) - client/release/FunConnect-1.1.0-Linux-x64.zip (99MB) - mobile JS bundles (android + ios) 已验证导出成功
This commit is contained in:
197
.gitea/workflows/build.yml
Normal file
197
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
name: Build All Platforms
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ==================== Desktop Client (Electron) ====================
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: client
|
||||||
|
- name: Build Windows (x64 NSIS + Portable)
|
||||||
|
run: npm run dist:win
|
||||||
|
working-directory: client
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-builds
|
||||||
|
path: |
|
||||||
|
client/release/*.exe
|
||||||
|
client/release/*.exe.blockmap
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: client
|
||||||
|
- name: Build macOS (x64 + arm64 DMG)
|
||||||
|
run: npm run dist:mac
|
||||||
|
working-directory: client
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-builds
|
||||||
|
path: |
|
||||||
|
client/release/*.dmg
|
||||||
|
client/release/*.dmg.blockmap
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: client
|
||||||
|
- name: Build Linux (AppImage + deb)
|
||||||
|
run: npm run dist:linux
|
||||||
|
working-directory: client
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Upload Linux artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-builds
|
||||||
|
path: |
|
||||||
|
client/release/*.AppImage
|
||||||
|
client/release/*.deb
|
||||||
|
|
||||||
|
# ==================== Mobile Client (Expo / React Native) ====================
|
||||||
|
build-android:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Generate native project
|
||||||
|
run: npx expo prebuild --platform android --no-install
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Build Android APK
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew assembleRelease
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Upload Android APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: android-builds
|
||||||
|
path: mobile/android/app/build/outputs/apk/release/*.apk
|
||||||
|
|
||||||
|
build-ios:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Generate native project
|
||||||
|
run: npx expo prebuild --platform ios --no-install
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Install CocoaPods
|
||||||
|
run: cd ios && pod install
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Build iOS (unsigned simulator)
|
||||||
|
run: |
|
||||||
|
xcodebuild \
|
||||||
|
-workspace ios/FunConnect.xcworkspace \
|
||||||
|
-scheme FunConnect \
|
||||||
|
-configuration Release \
|
||||||
|
-sdk iphonesimulator \
|
||||||
|
-derivedDataPath build \
|
||||||
|
CODE_SIGNING_ALLOWED=NO
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Package iOS build
|
||||||
|
run: |
|
||||||
|
cd build/Build/Products/Release-iphonesimulator
|
||||||
|
zip -r ../../../../FunConnect-ios-simulator.zip FunConnect.app
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Upload iOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ios-builds
|
||||||
|
path: mobile/FunConnect-ios-simulator.zip
|
||||||
|
|
||||||
|
# ==================== Mobile JS Bundles ====================
|
||||||
|
export-bundles:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Export Android bundle
|
||||||
|
run: npx expo export --platform android --output-dir release/android-bundle
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Export iOS bundle
|
||||||
|
run: npx expo export --platform ios --output-dir release/ios-bundle
|
||||||
|
working-directory: mobile
|
||||||
|
- name: Upload bundles
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mobile-bundles
|
||||||
|
path: |
|
||||||
|
mobile/release/android-bundle/
|
||||||
|
mobile/release/ios-bundle/
|
||||||
|
|
||||||
|
# ==================== Create Release ====================
|
||||||
|
release:
|
||||||
|
needs: [build-windows, build-macos, build-linux, build-android, export-bundles]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
- name: List artifacts
|
||||||
|
run: find artifacts -type f | head -50
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
artifacts/windows-builds/*
|
||||||
|
artifacts/macos-builds/*
|
||||||
|
artifacts/linux-builds/*
|
||||||
|
artifacts/android-builds/*
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
1
mobile/.gitignore
vendored
1
mobile/.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.expo/
|
.expo/
|
||||||
dist/
|
dist/
|
||||||
|
release/
|
||||||
*.jks
|
*.jks
|
||||||
*.p8
|
*.p8
|
||||||
*.p12
|
*.p12
|
||||||
|
|||||||
14311
mobile/package-lock.json
generated
Normal file
14311
mobile/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,18 +12,17 @@
|
|||||||
"build:all": "eas build --platform all"
|
"build:all": "eas build --platform all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-async-storage/async-storage": "1.21.0",
|
||||||
|
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||||
|
"@react-navigation/native": "^6.1.9",
|
||||||
"expo": "~50.0.0",
|
"expo": "~50.0.0",
|
||||||
"expo-status-bar": "~1.11.1",
|
|
||||||
"expo-clipboard": "~5.0.1",
|
"expo-clipboard": "~5.0.1",
|
||||||
"expo-constants": "~15.4.5",
|
"expo-constants": "~15.4.5",
|
||||||
|
"expo-status-bar": "~1.11.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.73.4",
|
"react-native": "0.73.4",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.8.2",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-screens": "~3.29.0"
|
||||||
"@react-navigation/native": "^6.1.9",
|
|
||||||
"@react-navigation/bottom-tabs": "^6.5.11",
|
|
||||||
"@react-native-async-storage/async-storage": "1.21.0",
|
|
||||||
"axios": "^1.6.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
export interface RoomInfo {
|
export interface RoomInfo {
|
||||||
@@ -28,54 +27,70 @@ export interface ServerStats {
|
|||||||
cluster: { totalNodes: number; onlineNodes: number; totalRooms: number; totalPlayers: number } | null;
|
cluster: { totalNodes: number; onlineNodes: number; totalRooms: number; totalPlayers: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function request(url: string, options?: RequestInit) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { ...options, signal: controller.signal });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
||||||
|
return data;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private http: AxiosInstance | null = null;
|
|
||||||
private baseUrl: string = '';
|
private baseUrl: string = '';
|
||||||
|
private configured: boolean = false;
|
||||||
|
|
||||||
configure(url: string) {
|
configure(url: string) {
|
||||||
this.baseUrl = url.replace(/\/+$/, '');
|
this.baseUrl = url.replace(/\/+$/, '');
|
||||||
this.http = axios.create({ baseURL: `${this.baseUrl}/api`, timeout: 10000 });
|
this.configured = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isConfigured() { return !!this.http; }
|
get isConfigured() { return this.configured; }
|
||||||
get serverUrl() { return this.baseUrl; }
|
get serverUrl() { return this.baseUrl; }
|
||||||
|
|
||||||
|
private api(path: string) { return `${this.baseUrl}/api${path}`; }
|
||||||
|
|
||||||
async getHealth(): Promise<ServerHealth> {
|
async getHealth(): Promise<ServerHealth> {
|
||||||
const res = await this.http!.get('/health');
|
return request(this.api('/health'));
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStats(): Promise<ServerStats> {
|
async getStats(): Promise<ServerStats> {
|
||||||
const res = await this.http!.get('/stats');
|
return request(this.api('/stats'));
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRooms(): Promise<{ rooms: RoomInfo[]; total: number }> {
|
async getRooms(): Promise<{ rooms: RoomInfo[]; total: number }> {
|
||||||
const res = await this.http!.get('/rooms');
|
return request(this.api('/rooms'));
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRoom(data: {
|
async createRoom(data: {
|
||||||
name: string; hostName: string; hostPort: number;
|
name: string; hostName: string; hostPort: number;
|
||||||
gameVersion: string; gameEdition: string; maxPlayers: number; password?: string;
|
gameVersion: string; gameEdition: string; maxPlayers: number; password?: string;
|
||||||
}) {
|
}) {
|
||||||
const res = await this.http!.post('/rooms', data);
|
return request(this.api('/rooms'), {
|
||||||
return res.data;
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinRoom(roomId: string, password?: string) {
|
async joinRoom(roomId: string, password?: string) {
|
||||||
const res = await this.http!.post(`/rooms/${roomId}/join`, { password });
|
return request(this.api(`/rooms/${roomId}/join`), {
|
||||||
return res.data;
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteRoom(roomId: string) {
|
async deleteRoom(roomId: string) {
|
||||||
const res = await this.http!.delete(`/rooms/${roomId}`);
|
return request(this.api(`/rooms/${roomId}`), { method: 'DELETE' });
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTraffic() {
|
async getTraffic() {
|
||||||
const res = await this.http!.get('/traffic');
|
return request(this.api('/traffic'));
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user