初始化,可用。
This commit is contained in:
commit
73b9e679e6
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust / Tauri
|
||||||
|
src-tauri/target
|
||||||
|
src-tauri/gen
|
||||||
100
README.md
Normal file
100
README.md
Normal 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
|
||||||
|
# 安装 rustup(https://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
63
docs/test-cases.md
Normal 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
21
index.html
Normal 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
1293
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal 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
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
25
src-tauri/Cargo.toml
Normal 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
4
src-tauri/build.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Tauri 构建脚本:在编译期生成必要的上下文与元数据
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
21
src-tauri/capabilities/default.json
Normal file
21
src-tauri/capabilities/default.json
Normal 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
BIN
src-tauri/icons/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
BIN
src-tauri/icons/128x128@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
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
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
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
59
src-tauri/src/lib.rs
Normal 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
7
src-tauri/src/main.rs
Normal 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
36
src-tauri/tauri.conf.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src-tauri/upload-keystore.jks
Normal file
BIN
src-tauri/upload-keystore.jks
Normal file
Binary file not shown.
209
src/main.js
Normal file
209
src/main.js
Normal 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
224
src/main.ts
Normal 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
91
src/styles.css
Normal 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
47
src/ui.js
Normal 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
85
src/ui.ts
Normal 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
16
tsconfig.json
Normal 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
17
vite.config.ts
Normal 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/**"] },
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user