修改架构,移动端优先。
save
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
(async () => {
|
||||
const u = new URL(process.env.DATABASE_URL);
|
||||
const c = await mysql.createConnection({
|
||||
host: u.hostname, port: Number(u.port),
|
||||
user: decodeURIComponent(u.username),
|
||||
password: decodeURIComponent(u.password),
|
||||
database: u.pathname.replace(/^\//, ''),
|
||||
});
|
||||
// 保留 id 最小的, 删掉其余重名行
|
||||
for (const t of ['Strategy', 'Indicator']) {
|
||||
const [r] = await c.query('DELETE FROM ' + t + ' WHERE id NOT IN (SELECT min_id FROM (SELECT MIN(id) AS min_id FROM ' + t + ' GROUP BY name) AS keep)');
|
||||
console.log('[' + t + '] 删除 ' + r.affectedRows + ' 条重复行');
|
||||
}
|
||||
await c.end();
|
||||
})().catch(e=>{console.error(e);process.exit(1)});
|
||||
@@ -0,0 +1,37 @@
|
||||
// 从 public/favicon.svg 派生 Tauri 需要的各尺寸 PNG 图标
|
||||
// 用法: node scripts/generate-icons.mjs
|
||||
// 一次性脚本, 生成的图标随仓库提交, 不需要每次都跑
|
||||
import sharp from 'sharp'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
const SVG = path.join(process.cwd(), 'public', 'favicon.svg')
|
||||
const OUT = path.join(process.cwd(), 'src-tauri', 'icons')
|
||||
fs.mkdirSync(OUT, { recursive: true })
|
||||
|
||||
const svg = fs.readFileSync(SVG)
|
||||
const base = sharp(svg)
|
||||
|
||||
// Tauri 桌面 + Android 需要的尺寸
|
||||
const TARGETS = [
|
||||
{ name: '32x32.png', size: 32 },
|
||||
{ name: '128x128.png', size: 128 },
|
||||
{ name: '128x128@2x.png', size: 256 },
|
||||
{ name: 'icon.png', size: 1024 },
|
||||
]
|
||||
|
||||
for (const t of TARGETS) {
|
||||
const buf = await base.clone().resize(t.size, t.size).png().toBuffer()
|
||||
fs.writeFileSync(path.join(OUT, t.name), buf)
|
||||
console.log('wrote', t.name, t.size + 'x' + t.size)
|
||||
}
|
||||
|
||||
// Windows .ico (单 256x256 frame, 简单够用)
|
||||
const ico = await sharp(svg).resize(256, 256).png().toBuffer()
|
||||
fs.writeFileSync(path.join(OUT, 'icon.ico'), ico)
|
||||
console.log('wrote icon.ico (256x256 PNG-as-ICO)')
|
||||
|
||||
// macOS .icns: TaurCli 可用 png2icns 工具, 这里只放占位
|
||||
// 如果未来要打 macOS DMG, 跑 cargo tauri icon 重新生成
|
||||
fs.copyFileSync(path.join(OUT, 'icon.png'), path.join(OUT, 'icon.icns'))
|
||||
console.log('wrote icon.icns (copy of icon.png, run cargo tauri icon to replace)')
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* iBoard 基础数据初始化脚本
|
||||
* 用法: npm run db:seed
|
||||
* 幂等: 主键/唯一键冲突跳过, Strategy/Indicator 按 name 存在则更新
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const crypto = require('crypto');
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
function hashPassword(plain) {
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hash = crypto.scryptSync(plain, salt, 64).toString('hex');
|
||||
return 'scrypt$' + salt + '$' + hash;
|
||||
}
|
||||
|
||||
function parseUrl(url) {
|
||||
const u = new URL(url);
|
||||
return {
|
||||
host: u.hostname,
|
||||
port: Number(u.port) || 3306,
|
||||
user: decodeURIComponent(u.username),
|
||||
password: decodeURIComponent(u.password),
|
||||
database: u.pathname.replace(/^\//, ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function insertIgnore(conn, sql, params) {
|
||||
try {
|
||||
const [r] = await conn.execute(sql, params);
|
||||
return r.affectedRows;
|
||||
} catch (e) {
|
||||
if (e.code === 'ER_DUP_ENTRY') return 0;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertByName(conn, table, payload, keyField) {
|
||||
const fields = Object.keys(payload);
|
||||
const placeholders = fields.map(() => '?').join(', ');
|
||||
const updateClause = fields
|
||||
.filter((f) => f !== keyField)
|
||||
.map((f) => '`' + f + '` = VALUES(`' + f + '`)')
|
||||
.join(', ');
|
||||
const sql = 'INSERT INTO `' + table + '` (' + fields.map((f) => '`' + f + '`').join(', ') + ') VALUES (' + placeholders + ') ON DUPLICATE KEY UPDATE ' + updateClause;
|
||||
const params = fields.map((f) => payload[f]);
|
||||
const [r] = await conn.execute(sql, params);
|
||||
return r.affectedRows;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cfg = parseUrl(process.env.DATABASE_URL);
|
||||
if (!cfg.host) throw new Error('DATABASE_URL 未配置或解析失败');
|
||||
const conn = await mysql.createConnection(cfg);
|
||||
console.log('已连接:', cfg.host + ':' + cfg.port + '/' + cfg.database);
|
||||
|
||||
const adminPwd = hashPassword('admin123');
|
||||
const n1 = await insertIgnore(
|
||||
conn,
|
||||
'INSERT INTO `User` (name, email, password, createdAt, updatedAt) VALUES (?, ?, ?, NOW(3), NOW(3))',
|
||||
['Admin', 'admin@iboard.local', adminPwd],
|
||||
);
|
||||
console.log('[User] 新增管理员: ' + n1 + ' 行 (admin@iboard.local / admin123)');
|
||||
|
||||
const strategies = [
|
||||
{ name: 'us-debt-gdp', description: '美国政府债务/GDP分析',
|
||||
data: { source: 'strategy.js', unit: '万亿美元', rows: [
|
||||
{ year: '2019', debt: 22.7, gdp: 21.4, debtToGdp: 106.1, interestToGdp: 1.8 },
|
||||
{ year: '2020', debt: 26.9, gdp: 20.9, debtToGdp: 128.7, interestToGdp: 1.6 },
|
||||
{ year: '2021', debt: 28.4, gdp: 23.0, debtToGdp: 123.5, interestToGdp: 1.5 },
|
||||
{ year: '2022', debt: 30.9, gdp: 25.4, debtToGdp: 121.6, interestToGdp: 1.6 },
|
||||
{ year: '2023', debt: 33.1, gdp: 26.9, debtToGdp: 123.0, interestToGdp: 1.9 },
|
||||
{ year: '2024', debt: 35.3, gdp: 28.5, debtToGdp: 123.9, interestToGdp: 2.1 },
|
||||
]},
|
||||
},
|
||||
{ name: 'china-debt-gdp', description: '中国政府债务/GDP分析',
|
||||
data: { source: 'strategy.js', unit: '万亿美元', rows: [
|
||||
{ year: '2019', debt: 8.8, gdp: 14.3, debtToGdp: 61.5, interestToGdp: 1.9 },
|
||||
{ year: '2020', debt: 10.3, gdp: 14.7, debtToGdp: 70.1, interestToGdp: 1.8 },
|
||||
{ year: '2021', debt: 11.7, gdp: 16.1, debtToGdp: 72.7, interestToGdp: 1.7 },
|
||||
{ year: '2022', debt: 13.1, gdp: 17.9, debtToGdp: 73.2, interestToGdp: 1.6 },
|
||||
{ year: '2023', debt: 14.5, gdp: 19.4, debtToGdp: 74.7, interestToGdp: 1.6 },
|
||||
{ year: '2024', debt: 15.9, gdp: 21.0, debtToGdp: 75.7, interestToGdp: 1.5 },
|
||||
]},
|
||||
},
|
||||
];
|
||||
let n2 = 0;
|
||||
for (const s of strategies) {
|
||||
n2 += await upsertByName(conn, 'Strategy',
|
||||
{ name: s.name, description: s.description, data: JSON.stringify(s.data), updatedAt: new Date() },
|
||||
'name');
|
||||
}
|
||||
console.log('[Strategy] upsert 策略: ' + n2 + ' 行 (1=新增, 2=更新)');
|
||||
|
||||
const indicators = [
|
||||
{ name: 'china-bond-1y', groupName: 'china-bond',
|
||||
data: { unit: '%', title: '中国国债1年', rows: [
|
||||
{ month: '2024-01', value: 1.85 }, { month: '2024-02', value: 1.82 },
|
||||
{ month: '2024-03', value: 1.78 }, { month: '2024-04', value: 1.75 },
|
||||
{ month: '2024-05', value: 1.72 }, { month: '2024-06', value: 1.68 },
|
||||
]}},
|
||||
{ name: 'china-bond-10y', groupName: 'china-bond',
|
||||
data: { unit: '%', title: '中国国债10年', rows: [
|
||||
{ month: '2024-01', value: 2.65 }, { month: '2024-02', value: 2.62 },
|
||||
{ month: '2024-03', value: 2.58 }, { month: '2024-04', value: 2.55 },
|
||||
{ month: '2024-05', value: 2.52 }, { month: '2024-06', value: 2.48 },
|
||||
]}},
|
||||
{ name: 'us-bond-1y', groupName: 'us-bond',
|
||||
data: { unit: '%', title: '美国国债1年', rows: [
|
||||
{ month: '2024-01', value: 4.85 }, { month: '2024-02', value: 4.82 },
|
||||
{ month: '2024-03', value: 4.78 }, { month: '2024-04', value: 4.65 },
|
||||
{ month: '2024-05', value: 4.55 }, { month: '2024-06', value: 4.45 },
|
||||
]}},
|
||||
{ name: 'china-lpr-1y', groupName: 'china-lpr',
|
||||
data: { unit: '%', title: '中国LPR 1年期', rows: [
|
||||
{ month: '2024-01', value: 3.45 }, { month: '2024-02', value: 3.45 },
|
||||
{ month: '2024-03', value: 3.45 }, { month: '2024-04', value: 3.40 },
|
||||
{ month: '2024-05', value: 3.40 }, { month: '2024-06', value: 3.40 },
|
||||
]}},
|
||||
];
|
||||
let n3 = 0;
|
||||
for (const i of indicators) {
|
||||
n3 += await upsertByName(conn, 'Indicator',
|
||||
{ name: i.name, groupName: i.groupName, data: JSON.stringify(i.data), updatedAt: new Date() },
|
||||
'name');
|
||||
}
|
||||
console.log('[Indicator] upsert 指标: ' + n3 + ' 行 (1=新增, 2=更新)');
|
||||
|
||||
const article = {
|
||||
title: '欢迎使用 iBoard',
|
||||
slug: 'welcome-to-iboard',
|
||||
content: 'iBoard 用于收集和整理经济数据,以策略为主体重新组织。\n\n本文为示例条目,正式文章仍由 content/articles/*.md 文件承载。',
|
||||
date: '2026-08-03',
|
||||
};
|
||||
const n4 = await insertIgnore(
|
||||
conn,
|
||||
'INSERT INTO `Article` (title, slug, content, date, createdAt, updatedAt) VALUES (?, ?, ?, ?, NOW(3), NOW(3))',
|
||||
[article.title, article.slug, article.content, article.date],
|
||||
);
|
||||
console.log('[Article] 新增示例文章: ' + n4 + ' 行');
|
||||
|
||||
const sowRows = [
|
||||
{ month: '2024-01', inventory: 4067 },
|
||||
{ month: '2024-02', inventory: 4042 },
|
||||
{ month: '2024-03', inventory: 3992 },
|
||||
{ month: '2024-04', inventory: 3986 },
|
||||
{ month: '2024-05', inventory: 3996 },
|
||||
{ month: '2024-06', inventory: 4038 },
|
||||
];
|
||||
let n5 = 0;
|
||||
for (const r of sowRows) {
|
||||
n5 += await insertIgnore(
|
||||
conn,
|
||||
'INSERT INTO `Econ_SowInventory` (month, inventory, createdAt, updatedAt) VALUES (?, ?, NOW(3), NOW(3))',
|
||||
[r.month, r.inventory],
|
||||
);
|
||||
}
|
||||
console.log('[Econ_SowInventory] 新增: ' + n5 + ' 行');
|
||||
|
||||
await conn.end();
|
||||
console.log('\nSeed 完成.');
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('Seed 失败:', e); process.exit(1); });
|
||||
@@ -0,0 +1,22 @@
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
(async () => {
|
||||
const u = new URL(process.env.DATABASE_URL);
|
||||
const c = await mysql.createConnection({
|
||||
host: u.hostname, port: Number(u.port),
|
||||
user: decodeURIComponent(u.username),
|
||||
password: decodeURIComponent(u.password),
|
||||
database: u.pathname.replace(/^\//, ''),
|
||||
});
|
||||
const [users] = await c.query('SELECT id,name,email,LEFT(password,20) AS pwd_prefix,createdAt FROM User');
|
||||
console.log('User:'); console.table(users);
|
||||
const [sow] = await c.query('SELECT * FROM Econ_SowInventory ORDER BY month');
|
||||
console.log('Econ_SowInventory:'); console.table(sow);
|
||||
const [str] = await c.query('SELECT id,name,description,JSON_LENGTH(data,"$.rows") AS row_count FROM Strategy');
|
||||
console.log('Strategy:'); console.table(str);
|
||||
const [ind] = await c.query('SELECT id,name,groupName,JSON_LENGTH(data,"$.rows") AS row_count FROM Indicator');
|
||||
console.log('Indicator:'); console.table(ind);
|
||||
const [art] = await c.query('SELECT id,title,slug,date FROM Article');
|
||||
console.log('Article:'); console.table(art);
|
||||
await c.end();
|
||||
})().catch(e=>{console.error(e);process.exit(1)});
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dotenv/config'
|
||||
import { PrismaMariaDb } from '@prisma/adapter-mariadb'
|
||||
import { PrismaClient } from '../generated/prisma/client.ts'
|
||||
|
||||
function parseDatabaseUrl(url) {
|
||||
const u = new URL(url)
|
||||
return {
|
||||
host: u.hostname,
|
||||
port: Number(u.port) || 3306,
|
||||
user: decodeURIComponent(u.username),
|
||||
password: decodeURIComponent(u.password),
|
||||
database: u.pathname.replace(/^\//, ''),
|
||||
}
|
||||
}
|
||||
|
||||
const p = new PrismaClient({ adapter: new PrismaMariaDb(parseDatabaseUrl(process.env.DATABASE_URL)) })
|
||||
try {
|
||||
const users = await p.user.findMany()
|
||||
console.log('User count:', users.length, '->', users.map((u) => u.email).join(','))
|
||||
const strategies = await p.strategy.findMany({ select: { id: true, name: true } })
|
||||
console.log('Strategy count:', strategies.length, '->', strategies.map((s) => s.name).join(','))
|
||||
const indicators = await p.indicator.findMany({ select: { id: true, name: true } })
|
||||
console.log('Indicator count:', indicators.length, '->', indicators.map((i) => i.name).join(','))
|
||||
const sow = await p.econ_SowInventory.findMany({ orderBy: { month: 'asc' } })
|
||||
console.log('SowInventory count:', sow.length, '->', sow.map((s) => s.month + '=' + s.inventory).join(','))
|
||||
} catch (e) {
|
||||
console.error('ERR:', e.message)
|
||||
process.exitCode = 1
|
||||
} finally {
|
||||
await p.$disconnect()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
(async () => {
|
||||
const url = new URL(process.env.DATABASE_URL);
|
||||
const conn = await mysql.createConnection({
|
||||
host: url.hostname,
|
||||
port: Number(url.port),
|
||||
user: decodeURIComponent(url.username),
|
||||
password: decodeURIComponent(url.password),
|
||||
database: url.pathname.replace(/^\//, ''),
|
||||
});
|
||||
const [tables] = await conn.query('SHOW TABLES');
|
||||
console.log('TABLES:');
|
||||
for (const row of tables) console.log(' -', Object.values(row)[0]);
|
||||
const tableNames = ['User', 'Article', 'Strategy', 'Indicator', 'Econ_SowInventory'];
|
||||
for (const t of tableNames) {
|
||||
const [r] = await conn.query('SELECT COUNT(*) AS c FROM ' + t + '');
|
||||
console.log(t + ': ' + r[0].c + ' rows');
|
||||
}
|
||||
await conn.end();
|
||||
})().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,15 @@
|
||||
const { PrismaClient } = require('../generated/prisma');
|
||||
(async () => {
|
||||
const p = new PrismaClient();
|
||||
const tables = await p.$queryRawUnsafe('SHOW TABLES');
|
||||
console.log('TABLES:', JSON.stringify(tables, null, 2));
|
||||
const counts = {
|
||||
User: await p.user.count(),
|
||||
Article: await p.article.count(),
|
||||
Strategy: await p.strategy.count(),
|
||||
Indicator: await p.indicator.count(),
|
||||
Econ_SowInventory: await p.econ_SowInventory.count(),
|
||||
};
|
||||
console.log('COUNTS:', JSON.stringify(counts, null, 2));
|
||||
await p.$disconnect();
|
||||
})().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'dotenv/config';
|
||||
import { PrismaClient } from '../generated/prisma/client.ts';
|
||||
|
||||
const url = process.env.DATABASE_URL;
|
||||
const p = new PrismaClient({ datasourceUrl: url });
|
||||
try {
|
||||
const tables = await p.$queryRawUnsafe('SHOW TABLES');
|
||||
console.log('TABLES:', JSON.stringify(tables, null, 2));
|
||||
const counts = {
|
||||
User: await p.user.count(),
|
||||
Article: await p.article.count(),
|
||||
Strategy: await p.strategy.count(),
|
||||
Indicator: await p.indicator.count(),
|
||||
Econ_SowInventory: await p.econ_SowInventory.count(),
|
||||
};
|
||||
console.log('COUNTS:', JSON.stringify(counts, null, 2));
|
||||
} catch (e) {
|
||||
console.error('ERR:', e.message);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await p.$disconnect();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# iBoard Android APK 打包
|
||||
# 在 WSL 内执行: bash scripts/wsl/build-apk.sh [debug|release]
|
||||
# 默认 debug, release 需要先配签名 (见末尾)
|
||||
set -euo pipefail
|
||||
|
||||
WIN_PROJECT='/mnt/d/workbench/iboard'
|
||||
cd "$WIN_PROJECT"
|
||||
|
||||
VARIANT="${1:-debug}"
|
||||
echo "构建变体: $VARIANT"
|
||||
|
||||
# 环境变量 (setup.sh 已写过, 防御性再写一次)
|
||||
export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
|
||||
export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/26.1.10909125}"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# 1. 先确保前端 dist/ 最新
|
||||
echo "=== 1. 构建前端 ==="
|
||||
npm run build
|
||||
|
||||
# 2. 进 src-tauri 跑 cargo tauri android build
|
||||
echo "=== 2. 构建 APK ==="
|
||||
cd src-tauri
|
||||
|
||||
if [ "$VARIANT" = "release" ]; then
|
||||
# release 需要签名, 首次请先生成 keystore:
|
||||
# keytool -genkey -v -keystore ~/iboard-release.keystore -alias iboard \
|
||||
# -keyalg RSA -keysize 2048 -validity 10000
|
||||
# 然后在 ~/.gradle/gradle.properties 配:
|
||||
# IBOARD_UPLOAD_STORE_FILE=/path/to/iboard-release.keystore
|
||||
# IBOARD_UPLOAD_KEY_ALIAS=iboard
|
||||
# IBOARD_UPLOAD_STORE_PASSWORD=...
|
||||
# IBOARD_UPLOAD_KEY_PASSWORD=...
|
||||
npx tauri android build --release
|
||||
else
|
||||
npx tauri android build --debug
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== 产物位置 ==="
|
||||
APK_DIR="$WIN_PROJECT/src-tauri/target/$( [ "$VARIANT" = "release" ] && echo release || echo debug )/bundle/android/app/build/outputs/apk/$VARIANT"
|
||||
ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK, 请检查上面日志)"
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 把刚打好的 APK 装到已连接的 adb 设备/模拟器
|
||||
# 用法: bash scripts/wsl/install-apk.sh [debug|release]
|
||||
set -euo pipefail
|
||||
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"
|
||||
adb install -r "$APK"
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# iBoard Android 端 WSL 首次搭建脚本
|
||||
# 在 WSL (Ubuntu 22.04+) 内执行: bash scripts/wsl/setup.sh
|
||||
# 完成后即可运行: bash scripts/wsl/build-apk.sh
|
||||
set -euo pipefail
|
||||
|
||||
# Windows 项目路径 (按需调整)
|
||||
WIN_PROJECT='/mnt/d/workbench/iboard'
|
||||
cd "$WIN_PROJECT"
|
||||
|
||||
echo "=== 1. 系统依赖 ==="
|
||||
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
|
||||
|
||||
echo "=== 2. Rust ==="
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source "$HOME/.cargo/env"
|
||||
fi
|
||||
rustc --version
|
||||
cargo --version
|
||||
|
||||
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
|
||||
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"
|
||||
fi
|
||||
export PATH="$HOME/android-sdk/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"
|
||||
|
||||
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 工程初始化 (首次执行) ==="
|
||||
cd "$WIN_PROJECT/src-tauri"
|
||||
if [ ! -d "gen" ]; then
|
||||
npx tauri android init
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== 搭建完成 ==="
|
||||
echo "接下来:"
|
||||
echo " bash scripts/wsl/build-apk.sh # 打 debug APK"
|
||||
echo " bash scripts/wsl/build-apk.sh release # 打 release APK (需要签名)"
|
||||
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"
|
||||
Reference in New Issue
Block a user