diff --git a/.env b/.env index e1e83b5..f5ca001 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL="mysql://root:fullstack@192.168.111.111:3306/iboard" \ No newline at end of file +DATABASE_URL="mysql://root:sunyard123@114.134.187.170:3306/iboard" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4e2331b..9e22894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ # =============================================================== # iBoard 一体化镜像: 静态前端 (dist/) + Express API (server/) 同进程 # 构建: docker build --build-arg DATABASE_URL=... -t iboard:latest . +# 可选 --build-arg VITE_API_BASE=https://api.example.com 让前端走绝对地址 # 运行: docker run -d --name iboard -p 3001:3001 -e DATABASE_URL=... iboard:latest # =============================================================== FROM node:22-alpine AS builder @@ -24,6 +25,9 @@ COPY . . # 构建前端 (输出到 dist/) 与 Prisma 客户端 RUN npx prisma generate +# build 时把 VITE_API_BASE 写进 bundle, 默认空 = 走相对路径 /api (同源 Express) +ARG VITE_API_BASE= +ENV VITE_API_BASE=$VITE_API_BASE RUN npm run build # 运行时阶段 diff --git a/scripts/wsl/build-apk.sh b/scripts/wsl/build-apk.sh index 5a8119d..648231b 100644 --- a/scripts/wsl/build-apk.sh +++ b/scripts/wsl/build-apk.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # iBoard Android APK 打包 # 用法: bash scripts/wsl/build-apk.sh [debug|release] +# release 模式自动构建 4 个 ABI 并出 universal APK set -euo pipefail WIN_PROJECT='/mnt/d/workbench/iboard' @@ -12,7 +13,15 @@ echo "构建变体: $VARIANT" # 环境变量 export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}" export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/27.2.12479018}" -export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH" +# 关键: 把 NDK toolchain bin 加进 PATH, .cargo/config.toml 里 +# linker = "aarch64-linux-android24-clang" 这种短名才能被解析到 +NDK_TOOLCHAIN_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" +if [ ! -d "$NDK_TOOLCHAIN_BIN" ]; then + echo "ERROR: NDK toolchain bin 不存在: $NDK_TOOLCHAIN_BIN" + echo "请确认 ANDROID_NDK_HOME 正确指向 NDK 27.2.x 目录" + exit 1 +fi +export PATH="$NDK_TOOLCHAIN_BIN:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH" # 0. 首次需要 android 工程 cd src-tauri @@ -22,6 +31,10 @@ if [ ! -d "gen/android" ]; then fi cd "$WIN_PROJECT" +# Android 后端地址, 写到前端 bundle 里 (覆盖默认的 /api 相对路径) +export VITE_API_BASE="${VITE_API_BASE:-http://bh.vps.honor3.com:3001}" +echo "VITE_API_BASE=$VITE_API_BASE" + # 1. 先构建前端, 让 Tauri android 工程能拿到 dist/ echo "=== 1. 构建前端 ===" npm run build @@ -36,30 +49,34 @@ RUST_BUILD_TASKS=( ":app:rustBuildX86_64Release" ":app:rustBuildI686Release" ) -RUST_BUILD_TASKS_DBG=( - ":app:rustBuildArm64Debug" - ":app:rustBuildArmv7Debug" - ":app:rustBuildX86_64Debug" - ":app:rustBuildI686Debug" -) SKIP_ARGS=() for t in "${RUST_BUILD_TASKS[@]}"; do SKIP_ARGS+=(-x "${t#:}"); done -if [ "$VARIANT" = "release" ]; then - echo "[release] 手工 cargo 编 lib, gradle 只跑 packaging (跳过 rustBuild* 避免 --release 重复 bug)" +# 4 个 Android 目标的 cargo target triple +ANDROID_TARGETS=( + "aarch64-linux-android" + "armv7-linux-androideabi" + "x86_64-linux-android" + "i686-linux-android" +) - echo " [cargo] build lib release ..." - cargo build \ - --target aarch64-linux-android \ - --features custom-protocol \ - --lib \ - --release +if [ "$VARIANT" = "release" ]; then + echo "[release] 手工 cargo 编 lib (4 个 ABI), gradle 只跑 packaging (跳过 rustBuild* 避免 --release 重复 bug)" + + for target in "${ANDROID_TARGETS[@]}"; do + echo " [cargo] build lib release for $target ..." + cargo build \ + --target "$target" \ + --features custom-protocol \ + --lib \ + --release + done echo " [gradle] assembleRelease -x rustBuild* ..." cd gen/android ./gradlew --no-daemon assembleRelease "${SKIP_ARGS[@]}" - APK_DIR="$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/release" + APK_DIR="$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/universal/release" echo echo "=== 产物 (未签名) ===" ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)" @@ -73,7 +90,12 @@ if [ "$VARIANT" = "release" ]; then echo " IBOARD_UPLOAD_KEY_ALIAS=iboard" echo " IBOARD_UPLOAD_STORE_PASSWORD=..." echo " IBOARD_UPLOAD_KEY_PASSWORD=..." - echo "3. 重跑本脚本" + echo "3. 重跑本脚本 (Gradle 读 gradle.properties 里的 IBOARD_UPLOAD_* 自动用 releaseSigning)" + echo + echo "=== 临时用 debug keystore 手动签 (仅测试) ===" + echo " \$ANDROID_HOME/build-tools/*/apksigner sign --ks ~/.android/debug.keystore \\" + echo " --ks-pass pass:android --key-pass pass:android \\" + echo " $APK_DIR/app-universal-release-unsigned.apk" else echo "[debug] Tauri CLI debug 路径 (不传 --release 不会触发 bug), 直接走" npx tauri android build --apk diff --git a/scripts/wsl/install-apk.sh b/scripts/wsl/install-apk.sh index 3f0e8c5..de95871 100644 --- a/scripts/wsl/install-apk.sh +++ b/scripts/wsl/install-apk.sh @@ -6,7 +6,32 @@ VARIANT="${1:-debug}" WIN_PROJECT='/mnt/d/workbench/iboard' export PATH="$HOME/android-sdk/platform-tools:$PATH" -APK_DIR="$WIN_PROJECT/src-tauri/target/$( [ "$VARIANT" = "release" ] && echo release || echo debug )/bundle/android/app/build/outputs/apk/$VARIANT" -APK=$(ls -1 "$APK_DIR"/*.apk | head -1) -echo "安装: $APK" +# 候选 APK 路径 (按优先级, 找到第一个就用) +CANDIDATES=( + # release 通用包 (脚本 build-apk.sh release 产物) + "$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk" + "$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk" + # 各 ABI 单独包 + "$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/arm64-v8a/release/app-arm64-v8a-release.apk" + "$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/arm64-v8a/release/app-arm64-v8a-release-unsigned.apk" + # debug 通用包 + "$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk" + # Tauri CLI debug 默认路径 + "$WIN_PROJECT/src-tauri/target/debug/bundle/android/app/build/outputs/apk/debug/app-debug.apk" + "$WIN_PROJECT/src-tauri/target/release/bundle/android/app/build/outputs/apk/release/app-release.apk" +) + +APK="" +for p in "${CANDIDATES[@]}"; do + if [ -f "$p" ]; then APK="$p"; break; fi +done + +if [ -z "$APK" ]; then + echo "ERROR: 找不到 $VARIANT APK, 请先 bash scripts/wsl/build-apk.sh $VARIANT" + echo "扫描过路径:" + for p in "${CANDIDATES[@]}"; do echo " $p"; done + exit 1 +fi + +echo "安装 ($VARIANT): $APK" adb install -r "$APK" \ No newline at end of file diff --git a/scripts/wsl/setup.sh b/scripts/wsl/setup.sh index 1f8f5c8..ac0f568 100644 --- a/scripts/wsl/setup.sh +++ b/scripts/wsl/setup.sh @@ -13,9 +13,7 @@ sudo apt update sudo apt install -y \ build-essential curl wget file libssl-dev pkg-config \ unzip zip git \ - openjdk-17-jdk-headless \ - clang libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev \ - libsoup-3.0-dev libjavascriptcoregtk-4.1-dev + openjdk-17-jdk-headless echo "=== 2. Rust ===" if ! command -v cargo >/dev/null 2>&1; then @@ -24,30 +22,33 @@ if ! command -v cargo >/dev/null 2>&1; then fi rustc --version cargo --version +# Tauri 2 + Android 需要 rustc 1.77+, 当前默认 toolchain 即可 echo "=== 3. Android SDK + NDK ===" -export ANDROID_HOME="$HOME/Android/Sdk" -export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$(ls -1 $ANDROID_HOME/ndk 2>/dev/null | sort -V | tail -1 || echo 26.1.10909125)" -mkdir -p "$HOME/android-sdk/cmdline-tools" -cd /tmp -if [ ! -d "$HOME/android-sdk/cmdline-tools/latest" ]; then +export ANDROID_HOME="$HOME/android-sdk" +mkdir -p "$ANDROID_HOME/cmdline-tools" +if [ ! -d "$ANDROID_HOME/cmdline-tools/latest" ]; then + cd /tmp echo "下载 Android cmdline-tools..." wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdline-tools.zip unzip -q cmdline-tools.zip - mv cmdline-tools "$HOME/android-sdk/cmdline-tools/latest" + mv cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" fi -export PATH="$HOME/android-sdk/cmdline-tools/latest/bin:$PATH" +export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" -yes | sdkmanager --licenses >/dev/null -sdkmanager --install "platforms;android-34" "build-tools;34.0.0" "platform-tools" \ - "ndk;26.1.10909125" "cmake;3.22.1" +yes | sdkmanager --licenses >/dev/null || true +# 与 src-tauri/gen/android 中 Tauri 生成的配置对齐: compileSdk=36, NDK 27.2 +sdkmanager --install "platforms;android-36" "build-tools;36.0.0" "platform-tools" \ + "ndk;27.2.12479018" "cmake;3.22.1" + +# 默认 NDK 版本 (build-apk.sh 也用这个) +export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.2.12479018" echo "ANDROID_HOME=$ANDROID_HOME" echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" echo "=== 4. Node 依赖 (与 Windows 共用 node_modules 也行, 这里重新装) ===" cd "$WIN_PROJECT" -# 跳过 Windows 二进制不兼容的 sharp 等, npm 会自动 rebuild npm install echo "=== 5. Tauri Android 工程初始化 (首次执行) ===" @@ -60,9 +61,18 @@ echo echo "=== 搭建完成 ===" echo "接下来:" echo " bash scripts/wsl/build-apk.sh # 打 debug APK" -echo " bash scripts/wsl/build-apk.sh release # 打 release APK (需要签名)" +echo " bash scripts/wsl/build-apk.sh release # 打 release APK (默认未签名, 需配置 keystore 才能签名)" +echo +echo "Release 签名 (可选, 仅 release 用):" +echo " 1. keytool -genkey -v -keystore ~/iboard-release.keystore -alias iboard -keyalg RSA -keysize 2048 -validity 10000" +echo " 2. 在 ~/.gradle/gradle.properties 写:" +echo " IBOARD_UPLOAD_STORE_FILE=/home/\$USER/iboard-release.keystore" +echo " IBOARD_UPLOAD_KEY_ALIAS=iboard" +echo " IBOARD_UPLOAD_STORE_PASSWORD=..." +echo " IBOARD_UPLOAD_KEY_PASSWORD=..." +echo " 3. 重跑 build-apk.sh release 即可" echo echo "如要永久生效, 把下面加进 ~/.bashrc:" echo " export ANDROID_HOME=$ANDROID_HOME" echo " export ANDROID_NDK_HOME=$ANDROID_NDK_HOME" -echo " export PATH=\$PATH:$HOME/android-sdk/cmdline-tools/latest/bin:$HOME/android-sdk/platform-tools" \ No newline at end of file +echo " export PATH=\$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:\$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" \ No newline at end of file diff --git a/src-tauri/.cargo/config.toml b/src-tauri/.cargo/config.toml new file mode 100644 index 0000000..89f50fe --- /dev/null +++ b/src-tauri/.cargo/config.toml @@ -0,0 +1,37 @@ +# iBoard Android 交叉编译配置 +# 让 cargo 在编译 Android 目标时强制用 NDK 提供的 clang/clang++ +# 否则会回退到宿主机 ld.bfd, 报 "file in wrong format" +# +# 链接器名直接写, 路径靠 PATH 解析 (在 build-apk.sh 里 export 过 NDK toolchain bin) + +[target.aarch64-linux-android] +linker = "aarch64-linux-android24-clang" +rustflags = [ + "-Clink-arg=-landroid", + "-Clink-arg=-llog", + "-Clink-arg=-lOpenSLES", +] + +[target.armv7-linux-androideabi] +linker = "armv7a-linux-androideabi24-clang" +rustflags = [ + "-Clink-arg=-landroid", + "-Clink-arg=-llog", + "-Clink-arg=-lOpenSLES", +] + +[target.x86_64-linux-android] +linker = "x86_64-linux-android24-clang" +rustflags = [ + "-Clink-arg=-landroid", + "-Clink-arg=-llog", + "-Clink-arg=-lOpenSLES", +] + +[target.i686-linux-android] +linker = "i686-linux-android24-clang" +rustflags = [ + "-Clink-arg=-landroid", + "-Clink-arg=-llog", + "-Clink-arg=-lOpenSLES", +] \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 92330b3..6f84c4f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -22,7 +22,7 @@ } ], "security": { - "csp": null + "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' data: blob: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost http://bh.vps.honor3.com:3001 https://bh.vps.honor3.com:3001; style-src 'self' 'unsafe-inline'; script-src 'self'" } }, "bundle": { diff --git a/src/api/client.js b/src/api/client.js index 2d2d4fb..f1e0a63 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -2,6 +2,12 @@ const TOKEN_KEY = 'iboard_token' const REMEMBER_KEY = 'iboard_remember' +// API 基址: +// - 默认相对路径 `/api` (web dev: Vite 代理; web prod: 同源 Express 一起托管) +// - 打包 Android 时设 VITE_API_BASE=http://bh.vps.honor3.com:3001 走外网后端 +const API_BASE = (import.meta.env.VITE_API_BASE || '').replace(/\/+$/, '') +const API_PREFIX = API_BASE ? API_BASE + '/api' : '/api' + export function getToken() { return localStorage.getItem(TOKEN_KEY) || '' } @@ -20,9 +26,13 @@ export function setRememberFlag(v) { else localStorage.removeItem(REMEMBER_KEY) } +export function getApiBase() { + return API_BASE +} + export async function api(path, { method = 'GET', body, headers = {} } = {}) { const token = getToken() - const res = await fetch('/api' + path, { + const res = await fetch(API_PREFIX + path, { method, headers: { 'Content-Type': 'application/json',