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:
FunMC
2026-02-23 08:16:28 +08:00
parent 09470c0465
commit 7fdc570391
5 changed files with 14547 additions and 24 deletions

1
mobile/.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules/
.expo/
dist/
release/
*.jks
*.p8
*.p12

14311
mobile/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,18 +12,17 @@
"build:all": "eas build --platform all"
},
"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-status-bar": "~1.11.1",
"expo-clipboard": "~5.0.1",
"expo-constants": "~15.4.5",
"expo-status-bar": "~1.11.1",
"react": "18.2.0",
"react-native": "0.73.4",
"react-native-safe-area-context": "4.8.2",
"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"
"react-native-screens": "~3.29.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View File

@@ -1,4 +1,3 @@
import axios, { AxiosInstance } from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
export interface RoomInfo {
@@ -28,54 +27,70 @@ export interface ServerStats {
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 {
private http: AxiosInstance | null = null;
private baseUrl: string = '';
private configured: boolean = false;
configure(url: string) {
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; }
private api(path: string) { return `${this.baseUrl}/api${path}`; }
async getHealth(): Promise<ServerHealth> {
const res = await this.http!.get('/health');
return res.data;
return request(this.api('/health'));
}
async getStats(): Promise<ServerStats> {
const res = await this.http!.get('/stats');
return res.data;
return request(this.api('/stats'));
}
async getRooms(): Promise<{ rooms: RoomInfo[]; total: number }> {
const res = await this.http!.get('/rooms');
return res.data;
return request(this.api('/rooms'));
}
async createRoom(data: {
name: string; hostName: string; hostPort: number;
gameVersion: string; gameEdition: string; maxPlayers: number; password?: string;
}) {
const res = await this.http!.post('/rooms', data);
return res.data;
return request(this.api('/rooms'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
async joinRoom(roomId: string, password?: string) {
const res = await this.http!.post(`/rooms/${roomId}/join`, { password });
return res.data;
return request(this.api(`/rooms/${roomId}/join`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
}
async deleteRoom(roomId: string) {
const res = await this.http!.delete(`/rooms/${roomId}`);
return res.data;
return request(this.api(`/rooms/${roomId}`), { method: 'DELETE' });
}
async getTraffic() {
const res = await this.http!.get('/traffic');
return res.data;
return request(this.api('/traffic'));
}
}