初始化,可用。

This commit is contained in:
cheney 2026-07-07 17:53:20 +08:00
commit 73b9e679e6
26 changed files with 8325 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
.DS_Store
# Rust / Tauri
src-tauri/target
src-tauri/gen

100
README.md Normal file
View File

@ -0,0 +1,100 @@
# Tauri Android 能力演示 App
一个基于 [Tauri v2](https://v2.tauri.app/) 的示例应用,用于集中演示 Tauri 在 Android 平台上可调用的常见原生能力。前端使用 Vite + TypeScript无框架后端为 Rust。
## 演示的能力
| 卡片 | 演示内容 | 依赖 |
| --- | --- | --- |
| Rust 命令调用 | 前端 `invoke` 调用 Rust 侧 `greet` / `system_summary` 命令 | `@tauri-apps/api` |
| 设备与系统信息 | 读取平台、系统版本、架构、主机名 | `plugin-os` |
| 原生对话框 | 消息框、确认框、文件选择器 | `plugin-dialog` |
| 系统通知 | 发送系统通知(含运行时权限申请) | `plugin-notification` |
| 文件读写 | 在应用私有目录读写文本文件 | `plugin-fs` |
| 剪贴板 | 读写系统剪贴板 | `plugin-clipboard-manager` |
| HTTP 请求 | 发起原生网络请求,绕过浏览器同源限制 | `plugin-http` |
## 目录结构
```
tauri-demo/
├── index.html # 页面骨架
├── package.json # 前端依赖与脚本
├── vite.config.ts # Vite 配置(固定端口 1420
├── tsconfig.json
├── src/ # 前端源码
│ ├── main.ts # 入口:定义并挂载所有能力卡片
│ ├── ui.ts # 卡片渲染组件
│ └── styles.css
└── src-tauri/ # Rust 后端与 Tauri 配置
├── Cargo.toml
├── build.rs
├── tauri.conf.json # 应用配置(含 bundle、identifier
├── capabilities/ # 权限capability声明
├── icons/ # 应用图标
└── src/
├── lib.rs # 命令与插件注册、移动端入口
└── main.rs # 桌面端入口
```
## 环境要求
构建 Android APK 需要以下工具链:
- Node.js 18+当前环境22
- Rust`rustup` + `cargo`)——**当前环境未安装**
- JDK 17——已就绪`C:\Users\occul\SynologyDrive\bin\jdk-17.0.10`
- Android SDK——已就绪`C:\Users\occul\SynologyDrive\bin\android-sdk-windows`
- Android NDK——**当前环境缺失,需安装**
- Android Platform API 33+ 与对应 build-tools——当前最高仅 `android-30`**需补装**
### 待补充安装项
1. **Rust 与 Android 目标**
```powershell
# 安装 rustuphttps://rustup.rs随后添加 Android 目标:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
```
2. **Android NDK 与新版平台**(用 SDK Manager 或 sdkmanager
```powershell
sdkmanager "ndk;26.1.10909125" "platforms;android-34" "build-tools;34.0.0"
```
## 环境变量
构建前需设置PowerShell 会话内示例):
```powershell
$env:JAVA_HOME = "C:\Users\occul\SynologyDrive\bin\jdk-17.0.10"
$env:ANDROID_HOME = "C:\Users\occul\SynologyDrive\bin\android-sdk-windows"
# NDK_HOME 指向实际安装的 NDK 版本目录
$env:NDK_HOME = "$env:ANDROID_HOME\ndk\26.1.10909125"
$env:Path = "$env:JAVA_HOME\bin;$env:USERPROFILE\.cargo\bin;$env:Path"
```
## 构建与运行
```powershell
# 1. 安装前端依赖
npm install
# 2. 桌面端预览(可选,用于快速验证 UI 与命令)
npm run tauri dev
# 3. 初始化 Android 工程(首次执行,生成 src-tauri/gen/android
npm run android:init
# 4. 连接设备/模拟器后实时调试
npm run android:dev
# 5. 打包 APK / AAB
npm run android:build
```
产物位于 `src-tauri/gen/android/app/build/outputs/` 下。
## 说明
- HTTP 卡片默认请求 `https://httpbin.org/get`,权限范围在 `src-tauri/capabilities/default.json` 中限定为 `https://*`
- 通知能力在 Android 13+ 需要运行时授权,首次点击会弹出系统权限框。
- 文件读写位于应用私有数据目录(`BaseDirectory.AppData`),不需要额外存储权限。

63
docs/test-cases.md Normal file
View File

@ -0,0 +1,63 @@
# 能力演示测试用例
由于本项目演示的多为原生设备能力(对话框、通知、剪贴板、文件、网络等),依赖真机/模拟器交互,采用手动验证清单。每条用例均带唯一编号。
## 1. 前端构建验证
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 1-1 | 类型检查 | 运行 `npx tsc --noEmit` | 退出码 0无类型错误 |
| 1-2 | 生产构建 | 运行 `npx vite build` | 生成 `dist/`,构建成功 |
## 2. Rust 命令调用
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 2-1 | greet 正常输入 | 输入“张三”后点击“调用 greet” | 显示“你好,张三!这条消息来自 Rust 后端 👋” |
| 2-2 | greet 空输入 | 清空输入后点击“调用 greet” | 名字回退为“世界” |
| 2-3 | 系统信息 | 点击“系统信息(Rust)” | 返回含 os、arch、app_version 的 JSON |
## 3. 设备与系统信息
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 3-1 | 读取信息 | 点击“读取信息” | 显示平台/类型/系统版本/架构/主机名 |
## 4. 原生对话框
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 4-1 | 消息框 | 点击“消息框” | 弹出原生消息框,关闭后显示“已显示消息框” |
| 4-2 | 确认框-确定 | 点击“确认框”并选“确定” | 显示“你选择了:确定” |
| 4-3 | 确认框-取消 | 点击“确认框”并选“取消” | 显示“你选择了:取消” |
| 4-4 | 文件选择 | 点击“选择文件”并选一个文件 | 显示所选文件路径 |
## 5. 系统通知
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 5-1 | 首次授权 | 首次点击“发送通知” | 弹出权限申请框 |
| 5-2 | 发送成功 | 授权后点击“发送通知” | 通知栏出现通知,显示“通知已发送” |
| 5-3 | 拒绝授权 | 拒绝权限后点击“发送通知” | 显示“未获得通知权限” |
## 6. 文件读写
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 6-1 | 写入 | 输入文本点击“写入文件” | 显示“已写入 tauri-demo.txt” |
| 6-2 | 读取 | 写入后点击“读取文件” | 显示刚写入的内容 |
| 6-3 | 读取不存在 | 全新环境直接点击“读取文件” | 显示“文件不存在,请先写入” |
## 7. 剪贴板
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 7-1 | 写入 | 输入内容点击“复制” | 显示“已写入剪贴板” |
| 7-2 | 读取 | 复制后点击“粘贴读取” | 显示刚复制的内容 |
## 8. HTTP 请求
| 编号 | 用例 | 操作 | 预期结果 |
| --- | --- | --- | --- |
| 8-1 | GET 成功 | 使用默认地址点击“GET 请求” | 显示状态码 200 与响应预览 |
| 8-2 | 非法地址 | 输入无效地址后点击“GET 请求” | 输出区显示红色错误信息 |

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Tauri 能力演示</title>
</head>
<body>
<div id="app">
<header class="app-header">
<h1>Tauri Android 能力演示</h1>
<p id="platform-line" class="subtitle">正在加载平台信息…</p>
</header>
<main id="cards"></main>
<footer class="app-footer">
<p>点击各卡片按钮体验 Tauri 提供的原生能力</p>
</footer>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1293
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "tauri-demo",
"version": "0.1.0",
"description": "Tauri Android 能力演示 App",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"android:init": "tauri android init",
"android:dev": "tauri android dev",
"android:build": "tauri android build"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.0.0",
"@tauri-apps/plugin-http": "^2.0.0",
"@tauri-apps/plugin-notification": "^2.0.0",
"@tauri-apps/plugin-os": "^2.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"typescript": "^5.6.0",
"vite": "^5.4.0"
}
}

5971
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "tauri-demo"
version = "0.1.0"
description = "Tauri Android 能力演示 App"
edition = "2021"
rust-version = "1.77.2"
[lib]
# 移动端要求以库形式构建,名称需与 tauri.conf 中保持一致
name = "tauri_demo_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-os = "2"
tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-fs = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

4
src-tauri/build.rs Normal file
View File

@ -0,0 +1,4 @@
// Tauri 构建脚本:在编译期生成必要的上下文与元数据
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,21 @@
{
"$schema": "../gen/schemas/mobile-schema.json",
"identifier": "default",
"description": "演示 App 所需的能力集合",
"windows": ["main"],
"webviews": ["main"],
"permissions": [
"core:default",
"os:default",
"dialog:default",
"notification:default",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"fs:allow-app-read-recursive",
"fs:allow-app-write-recursive",
{
"identifier": "http:default",
"allow": [{ "url": "https://*" }]
}
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

59
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,59 @@
use serde::Serialize;
/// 系统信息返回结构,序列化为 JSON 传回前端
#[derive(Serialize)]
struct SysInfo {
/// 目标操作系统(如 android、windows
os: String,
/// 目标 CPU 架构(如 aarch64、x86_64
arch: String,
/// 应用版本号
app_version: String,
}
/// 向前端返回一句问候语
///
/// # 参数
/// - `name`: 前端传入的名字
///
/// # 返回
/// 拼接后的中文问候字符串
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好,{}!这条消息来自 Rust 后端 👋", name)
}
/// 汇总当前运行环境的系统信息
///
/// # 返回
/// 格式化后的 JSON 字符串(包含 os、arch、app_version
///
/// # 注意
/// os 与 arch 取自编译期常量,反映的是构建目标平台。
#[tauri::command]
fn system_summary() -> String {
let info = SysInfo {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
app_version: env!("CARGO_PKG_VERSION").to_string(),
};
serde_json::to_string_pretty(&info).unwrap_or_else(|_| "{}".to_string())
}
/// 应用运行入口:注册插件与命令并启动 Tauri
///
/// # 注意
/// 使用 `#[cfg_attr(mobile, ...)]` 标记,移动端会由平台生成的入口调用此函数。
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_http::init())
.invoke_handler(tauri::generate_handler![greet, system_summary])
.run(tauri::generate_context!())
.expect("启动 Tauri 应用失败");
}

7
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,7 @@
// 桌面端可执行文件入口:直接调用库中的 run 函数
// 移动端不会使用此文件,而是通过生成的原生工程调用 lib 中的入口。
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri_demo_lib::run()
}

36
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,36 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "TauriDemo",
"version": "0.1.0",
"identifier": "com.tauridemo.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "Tauri 能力演示",
"width": 420,
"height": 780,
"resizable": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.png",
"icons/icon.ico"
]
}
}

Binary file not shown.

209
src/main.js Normal file
View File

@ -0,0 +1,209 @@
import "./styles.css";
import { renderCard } from "./ui";
import { invoke } from "@tauri-apps/api/core";
import { platform, version, arch, type, hostname } from "@tauri-apps/plugin-os";
import { message, ask, open as openDialog } from "@tauri-apps/plugin-dialog";
import { isPermissionGranted, requestPermission, sendNotification, } from "@tauri-apps/plugin-notification";
import { writeTextFile, readTextFile, exists, BaseDirectory, } from "@tauri-apps/plugin-fs";
import { writeText as writeClipboard, readText as readClipboard, } from "@tauri-apps/plugin-clipboard-manager";
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
/** 演示用的本地文件名,统一保存在应用数据目录下 */
const DEMO_FILE = "tauri-demo.txt";
/**
* 确保已获得系统通知权限未授权时向用户申请
* @returns 是否已获得权限
* 注意事项Android 13+ 需要运行时授权首次调用会弹出系统权限框
*/
async function ensureNotifyPermission() {
let granted = await isPermissionGranted();
if (!granted) {
const result = await requestPermission();
granted = result === "granted";
}
return granted;
}
/** 所有能力演示卡片的定义 */
const cards = [
{
id: "invoke",
title: "Rust 命令调用",
desc: "前端通过 invoke 调用 Rust 侧定义的命令,演示 JS 与原生的双向通信。",
input: { placeholder: "输入你的名字", default: "Tauri" },
actions: [
{
label: "调用 greet",
run: async (ctx) => await invoke("greet", { name: ctx.getInput() || "世界" }),
},
{
label: "系统信息(Rust)",
secondary: true,
run: async () => await invoke("system_summary"),
},
],
},
{
id: "os",
title: "设备与系统信息",
desc: "使用 os 插件读取平台、版本、架构等运行环境信息。",
actions: [
{
label: "读取信息",
run: async () => {
const info = {
平台: platform(),
类型: type(),
系统版本: version(),
架构: arch(),
主机名: hostname(),
};
return JSON.stringify(info, null, 2);
},
},
],
},
{
id: "dialog",
title: "原生对话框",
desc: "调用系统原生的消息框与确认框,以及文件选择器。",
actions: [
{
label: "消息框",
run: async () => {
await message("这是一个来自 Tauri 的原生消息框!", {
title: "提示",
kind: "info",
});
return "已显示消息框";
},
},
{
label: "确认框",
secondary: true,
run: async () => {
const yes = await ask("确定要继续吗?", { title: "请确认" });
return `你选择了:${yes ? "确定" : "取消"}`;
},
},
{
label: "选择文件",
secondary: true,
run: async () => {
const path = await openDialog({ multiple: false });
return path ? `已选择:${path}` : "未选择文件";
},
},
],
},
{
id: "notify",
title: "系统通知",
desc: "向系统通知栏发送一条通知,首次使用会申请通知权限。",
input: { placeholder: "通知内容", default: "你好,来自 Tauri" },
actions: [
{
label: "发送通知",
run: async (ctx) => {
if (!(await ensureNotifyPermission()))
return "未获得通知权限";
sendNotification({
title: "Tauri 演示",
body: ctx.getInput() || "你好,来自 Tauri",
});
return "通知已发送,请查看通知栏";
},
},
],
},
{
id: "fs",
title: "文件读写",
desc: "在应用私有数据目录下写入与读取文本文件,演示文件系统访问。",
input: { placeholder: "要写入的文本", default: "Hello Tauri FS" },
actions: [
{
label: "写入文件",
run: async (ctx) => {
await writeTextFile(DEMO_FILE, ctx.getInput() || "空内容", {
baseDir: BaseDirectory.AppData,
});
return `已写入 ${DEMO_FILE}`;
},
},
{
label: "读取文件",
secondary: true,
run: async () => {
const has = await exists(DEMO_FILE, { baseDir: BaseDirectory.AppData });
if (!has)
return "文件不存在,请先写入";
const content = await readTextFile(DEMO_FILE, {
baseDir: BaseDirectory.AppData,
});
return `读取内容:\n${content}`;
},
},
],
},
{
id: "clipboard",
title: "剪贴板",
desc: "读写系统剪贴板内容,演示与系统剪贴板的交互。",
input: { placeholder: "要复制的内容", default: "Tauri 剪贴板演示" },
actions: [
{
label: "复制",
run: async (ctx) => {
await writeClipboard(ctx.getInput() || "");
return "已写入剪贴板";
},
},
{
label: "粘贴读取",
secondary: true,
run: async () => {
const text = await readClipboard();
return `剪贴板内容:\n${text}`;
},
},
],
},
{
id: "http",
title: "HTTP 请求",
desc: "通过 http 插件发起原生网络请求,不受浏览器同源策略限制。",
input: { placeholder: "请求地址", default: "https://httpbin.org/get" },
actions: [
{
label: "GET 请求",
run: async (ctx) => {
const url = ctx.getInput() || "https://httpbin.org/get";
const resp = await tauriFetch(url, { method: "GET" });
const text = await resp.text();
const preview = text.length > 500 ? text.slice(0, 500) + "…" : text;
return `状态码:${resp.status}\n${preview}`;
},
},
],
},
];
/**
* 应用入口渲染顶部平台信息并挂载所有能力卡片
* 注意事项读取平台信息可能在纯浏览器环境下失败此时回退为提示文案
*/
async function bootstrap() {
const platformLine = document.querySelector("#platform-line");
if (platformLine) {
try {
platformLine.textContent = `当前平台:${platform()} · ${arch()} · 系统 ${version()}`;
}
catch {
platformLine.textContent = "运行于非 Tauri 环境,请在 App 内查看完整能力。";
}
}
const container = document.querySelector("#cards");
if (!container)
return;
for (const card of cards)
renderCard(container, card);
}
bootstrap();

224
src/main.ts Normal file
View File

@ -0,0 +1,224 @@
import "./styles.css";
import { renderCard, type DemoCard } from "./ui";
import { invoke } from "@tauri-apps/api/core";
import { platform, version, arch, type, hostname } from "@tauri-apps/plugin-os";
import { message, ask, open as openDialog } from "@tauri-apps/plugin-dialog";
import {
isPermissionGranted,
requestPermission,
sendNotification,
} from "@tauri-apps/plugin-notification";
import {
writeTextFile,
readTextFile,
exists,
BaseDirectory,
} from "@tauri-apps/plugin-fs";
import {
writeText as writeClipboard,
readText as readClipboard,
} from "@tauri-apps/plugin-clipboard-manager";
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
/** 演示用的本地文件名,统一保存在应用数据目录下 */
const DEMO_FILE = "tauri-demo.txt";
/**
*
* @returns
* Android 13+
*/
async function ensureNotifyPermission(): Promise<boolean> {
let granted = await isPermissionGranted();
if (!granted) {
const result = await requestPermission();
granted = result === "granted";
}
return granted;
}
/** 所有能力演示卡片的定义 */
const cards: DemoCard[] = [
{
id: "invoke",
title: "Rust 命令调用",
desc: "前端通过 invoke 调用 Rust 侧定义的命令,演示 JS 与原生的双向通信。",
input: { placeholder: "输入你的名字", default: "Tauri" },
actions: [
{
label: "调用 greet",
run: async (ctx) =>
await invoke<string>("greet", { name: ctx.getInput() || "世界" }),
},
{
label: "系统信息(Rust)",
secondary: true,
run: async () => await invoke<string>("system_summary"),
},
],
},
{
id: "os",
title: "设备与系统信息",
desc: "使用 os 插件读取平台、版本、架构等运行环境信息。",
actions: [
{
label: "读取信息",
run: async () => {
const info = {
平台: platform(),
类型: type(),
系统版本: version(),
架构: arch(),
主机名: hostname(),
};
return JSON.stringify(info, null, 2);
},
},
],
},
{
id: "dialog",
title: "原生对话框",
desc: "调用系统原生的消息框与确认框,以及文件选择器。",
actions: [
{
label: "消息框",
run: async () => {
await message("这是一个来自 Tauri 的原生消息框!", {
title: "提示",
kind: "info",
});
return "已显示消息框";
},
},
{
label: "确认框",
secondary: true,
run: async () => {
const yes = await ask("确定要继续吗?", { title: "请确认" });
return `你选择了:${yes ? "确定" : "取消"}`;
},
},
{
label: "选择文件",
secondary: true,
run: async () => {
const path = await openDialog({ multiple: false });
return path ? `已选择:${path}` : "未选择文件";
},
},
],
},
{
id: "notify",
title: "系统通知",
desc: "向系统通知栏发送一条通知,首次使用会申请通知权限。",
input: { placeholder: "通知内容", default: "你好,来自 Tauri" },
actions: [
{
label: "发送通知",
run: async (ctx) => {
if (!(await ensureNotifyPermission())) return "未获得通知权限";
sendNotification({
title: "Tauri 演示",
body: ctx.getInput() || "你好,来自 Tauri",
});
return "通知已发送,请查看通知栏";
},
},
],
},
{
id: "fs",
title: "文件读写",
desc: "在应用私有数据目录下写入与读取文本文件,演示文件系统访问。",
input: { placeholder: "要写入的文本", default: "Hello Tauri FS" },
actions: [
{
label: "写入文件",
run: async (ctx) => {
await writeTextFile(DEMO_FILE, ctx.getInput() || "空内容", {
baseDir: BaseDirectory.AppData,
});
return `已写入 ${DEMO_FILE}`;
},
},
{
label: "读取文件",
secondary: true,
run: async () => {
const has = await exists(DEMO_FILE, { baseDir: BaseDirectory.AppData });
if (!has) return "文件不存在,请先写入";
const content = await readTextFile(DEMO_FILE, {
baseDir: BaseDirectory.AppData,
});
return `读取内容:\n${content}`;
},
},
],
},
{
id: "clipboard",
title: "剪贴板",
desc: "读写系统剪贴板内容,演示与系统剪贴板的交互。",
input: { placeholder: "要复制的内容", default: "Tauri 剪贴板演示" },
actions: [
{
label: "复制",
run: async (ctx) => {
await writeClipboard(ctx.getInput() || "");
return "已写入剪贴板";
},
},
{
label: "粘贴读取",
secondary: true,
run: async () => {
const text = await readClipboard();
return `剪贴板内容:\n${text}`;
},
},
],
},
{
id: "http",
title: "HTTP 请求",
desc: "通过 http 插件发起原生网络请求,不受浏览器同源策略限制。",
input: { placeholder: "请求地址", default: "https://httpbin.org/get" },
actions: [
{
label: "GET 请求",
run: async (ctx) => {
const url = ctx.getInput() || "https://httpbin.org/get";
const resp = await tauriFetch(url, { method: "GET" });
const text = await resp.text();
const preview = text.length > 500 ? text.slice(0, 500) + "…" : text;
return `状态码:${resp.status}\n${preview}`;
},
},
],
},
];
/**
*
* 退
*/
async function bootstrap(): Promise<void> {
const platformLine = document.querySelector<HTMLParagraphElement>("#platform-line");
if (platformLine) {
try {
platformLine.textContent = `当前平台:${platform()} · ${arch()} · 系统 ${version()}`;
} catch {
platformLine.textContent = "运行于非 Tauri 环境,请在 App 内查看完整能力。";
}
}
const container = document.querySelector<HTMLElement>("#cards");
if (!container) return;
for (const card of cards) renderCard(container, card);
}
bootstrap();

91
src/styles.css Normal file
View File

@ -0,0 +1,91 @@
:root {
--bg: #0f1220;
--card: #1a1f36;
--accent: #7c5cff;
--accent-2: #3ad1c4;
--text: #e7e9f3;
--muted: #9aa0bd;
--ok: #3ad17a;
--err: #ff6b6b;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
-webkit-tap-highlight-color: transparent;
}
#app {
max-width: 720px;
margin: 0 auto;
padding: env(safe-area-inset-top, 16px) 16px 32px;
}
.app-header { padding: 20px 4px 8px; }
.app-header h1 { font-size: 22px; margin: 0 0 6px; }
.subtitle { color: var(--muted); margin: 0; font-size: 13px; line-height: 1.5; }
#cards {
display: grid;
gap: 14px;
margin-top: 16px;
}
.card {
background: var(--card);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
padding: 16px;
}
.card h2 { font-size: 16px; margin: 0 0 4px; }
.card p.desc { color: var(--muted); font-size: 13px; margin: 0 0 12px; line-height: 1.5; }
.row { display: flex; gap: 8px; flex-wrap: wrap; }
button {
appearance: none;
border: none;
border-radius: 10px;
padding: 10px 14px;
font-size: 14px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
cursor: pointer;
transition: transform 0.05s ease, opacity 0.15s ease;
}
button:active { transform: scale(0.97); }
button.secondary { background: rgba(255, 255, 255, 0.08); }
input[type="text"] {
flex: 1;
min-width: 140px;
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 10px 12px;
color: var(--text);
font-size: 14px;
}
.output {
margin-top: 12px;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 10px 12px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12.5px;
white-space: pre-wrap;
word-break: break-word;
min-height: 20px;
color: var(--muted);
}
.output.ok { color: var(--ok); }
.output.err { color: var(--err); }
.app-footer { margin-top: 24px; text-align: center; color: var(--muted); font-size: 12px; }

47
src/ui.js Normal file
View File

@ -0,0 +1,47 @@
/**
* 渲染单个能力卡片到指定容器
* @param container 卡片挂载的父节点
* @param card 卡片定义
* 注意事项按钮点击时会自动捕获异常并以红色输出错误信息避免未处理的 Promise 报错
*/
export function renderCard(container, card) {
const el = document.createElement("section");
el.className = "card";
const inputHtml = card.input
? `<div class="row" style="margin-bottom:10px">
<input type="text" id="input-${card.id}" placeholder="${card.input.placeholder}" value="${card.input.default ?? ""}" />
</div>`
: "";
el.innerHTML = `
<h2>${card.title}</h2>
<p class="desc">${card.desc}</p>
${inputHtml}
<div class="row" id="actions-${card.id}"></div>
<div class="output" id="out-${card.id}"></div>
`;
container.appendChild(el);
const out = el.querySelector(`#out-${card.id}`);
const actionsRow = el.querySelector(`#actions-${card.id}`);
const ctx = {
getInput: () => el.querySelector(`#input-${card.id}`)?.value ?? "",
};
for (const action of card.actions) {
const btn = document.createElement("button");
btn.textContent = action.label;
if (action.secondary)
btn.classList.add("secondary");
btn.addEventListener("click", async () => {
out.className = "output";
out.textContent = "执行中…";
try {
out.textContent = await action.run(ctx);
out.classList.add("ok");
}
catch (err) {
out.textContent = `出错:${err instanceof Error ? err.message : String(err)}`;
out.classList.add("err");
}
});
actionsRow.appendChild(btn);
}
}

85
src/ui.ts Normal file
View File

@ -0,0 +1,85 @@
/**
*
* - title: 卡片标题
* - desc: 能力说明
* - actions: 该卡片下的操作按钮列表
*/
export interface DemoAction {
/** 按钮文字 */
label: string;
/** 是否使用次要样式(灰色按钮) */
secondary?: boolean;
/** 点击后执行的处理函数,返回要展示的文本 */
run: (ctx: DemoContext) => Promise<string> | string;
}
/** 卡片定义 */
export interface DemoCard {
/** 卡片唯一 id用于关联输出区域 */
id: string;
/** 卡片标题 */
title: string;
/** 卡片说明文字 */
desc: string;
/** 是否需要一个文本输入框 */
input?: { placeholder: string; default?: string };
/** 操作按钮 */
actions: DemoAction[];
}
/** 传递给操作函数的上下文,可读取输入框内容 */
export interface DemoContext {
/** 获取当前卡片输入框的值(不存在时返回空字符串) */
getInput: () => string;
}
/**
*
* @param container
* @param card
* Promise
*/
export function renderCard(container: HTMLElement, card: DemoCard): void {
const el = document.createElement("section");
el.className = "card";
const inputHtml = card.input
? `<div class="row" style="margin-bottom:10px">
<input type="text" id="input-${card.id}" placeholder="${card.input.placeholder}" value="${card.input.default ?? ""}" />
</div>`
: "";
el.innerHTML = `
<h2>${card.title}</h2>
<p class="desc">${card.desc}</p>
${inputHtml}
<div class="row" id="actions-${card.id}"></div>
<div class="output" id="out-${card.id}"></div>
`;
container.appendChild(el);
const out = el.querySelector<HTMLDivElement>(`#out-${card.id}`)!;
const actionsRow = el.querySelector<HTMLDivElement>(`#actions-${card.id}`)!;
const ctx: DemoContext = {
getInput: () =>
el.querySelector<HTMLInputElement>(`#input-${card.id}`)?.value ?? "",
};
for (const action of card.actions) {
const btn = document.createElement("button");
btn.textContent = action.label;
if (action.secondary) btn.classList.add("secondary");
btn.addEventListener("click", async () => {
out.className = "output";
out.textContent = "执行中…";
try {
out.textContent = await action.run(ctx);
out.classList.add("ok");
} catch (err) {
out.textContent = `出错:${err instanceof Error ? err.message : String(err)}`;
out.classList.add("err");
}
});
actionsRow.appendChild(btn);
}
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"isolatedModules": true,
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": []
},
"include": ["src"]
}

17
vite.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from "vite";
// Tauri 期望前端服务运行在固定端口,且在移动端调试时监听所有地址
const host = process.env.TAURI_DEV_HOST;
export default defineConfig({
clearScreen: false,
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? { protocol: "ws", host, port: 1421 }
: undefined,
watch: { ignored: ["**/src-tauri/**"] },
},
});