From 6ba79a99d3d105aaa4a797ac81d88ab2febf90d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Thu, 18 Jun 2026 11:42:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20web=20=E7=AB=AF=20?= =?UTF-8?q?=E2=80=94=20axum=20HTTP=20=E6=9C=8D=E5=8A=A1=20+=20Docker=20?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 workspace crate : - axum 0.8 + tokio 异步 HTTP 服务 - / → 嵌入式 HTML 页面(输入→实时预览→下载/复制) - /api/qr?text=&level=M&margin=4&size=8 → PNG - Dockerfile: rust-alpine 多阶段构建,镜像仅 ~12MB - Cargo.toml: workspace 新增 web 成员 部署: docker build -t qrgen-web -f web/Dockerfile . docker run -p 3000:3000 qrgen-web Co-Authored-By: Claude --- Cargo.lock | 120 ++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- web/Cargo.toml | 14 ++++ web/Dockerfile | 18 ++++++ web/src/main.rs | 80 +++++++++++++++++++++++ web/src/templates/index.html | 122 +++++++++++++++++++++++++++++++++++ 6 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 web/Cargo.toml create mode 100644 web/Dockerfile create mode 100644 web/src/main.rs create mode 100644 web/src/templates/index.html diff --git a/Cargo.lock b/Cargo.lock index f3bce30..bafa83c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,58 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.21.7" @@ -1458,6 +1510,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.10.1" @@ -1471,6 +1529,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1927,6 +1986,12 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.2" @@ -2601,6 +2666,18 @@ dependencies = [ "tauri-plugin-store", ] +[[package]] +name = "qrgen-web" +version = "0.1.0" +dependencies = [ + "axum", + "qr-core", + "serde", + "serde_json", + "tokio", + "tower-http", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -2804,6 +2881,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2965,6 +3048,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -2994,6 +3088,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -3074,6 +3180,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3744,7 +3860,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -3907,6 +4025,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3945,6 +4064,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 2708994..2882756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["core", "cli", "gui"] +members = ["core", "cli", "gui", "web"] [workspace.package] version = "0.1.0" diff --git a/web/Cargo.toml b/web/Cargo.toml new file mode 100644 index 0000000..e516fb8 --- /dev/null +++ b/web/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "qrgen-web" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +qr-core = { path = "../core" } +axum = "0.8" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tower-http = { version = "0.6", features = ["cors"] } diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..97233bf --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,18 @@ +# ── 构建阶段 ── +FROM rust:1.95-alpine AS builder + +RUN apk add --no-cache musl-dev +WORKDIR /app +COPY . . +RUN cargo build --release -p qrgen-web + +# ── 运行阶段 ── +FROM alpine:3.22 + +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai + +COPY --from=builder /app/target/release/qrgen-web /usr/local/bin/qrgen-web + +EXPOSE 3000 +CMD ["qrgen-web"] diff --git a/web/src/main.rs b/web/src/main.rs new file mode 100644 index 0000000..6331355 --- /dev/null +++ b/web/src/main.rs @@ -0,0 +1,80 @@ +use axum::{ + extract::Query, + http::{header, StatusCode}, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use qr_core::qr::{QrCode, QrConfig, VersionMode}; +use qr_core::version::EcLevel; +use serde::Deserialize; + +/// GET /api/qr 查询参数 +#[derive(Deserialize)] +struct QrParams { + text: String, + #[serde(default = "default_level")] + level: String, + #[serde(default = "default_margin")] + margin: u8, + #[serde(default = "default_size")] + size: u8, +} + +fn default_level() -> String { "M".into() } +fn default_margin() -> u8 { 4 } +fn default_size() -> u8 { 8 } + +fn parse_level(s: &str) -> Result { + match s.to_uppercase().as_str() { + "L" => Ok(EcLevel::L), + "M" => Ok(EcLevel::M), + "Q" => Ok(EcLevel::Q), + "H" => Ok(EcLevel::H), + _ => Err(format!("无效纠错级别: {},支持 L/M/Q/H", s)), + } +} + +/// 首页 HTML(编译期嵌入) +async fn index() -> Html<&'static str> { + Html(include_str!("templates/index.html")) +} + +/// QR 码生成 API → PNG 图片 +async fn generate_qr(Query(params): Query) -> impl IntoResponse { + let level = match parse_level(¶ms.level) { + Ok(l) => l, + Err(e) => return (StatusCode::BAD_REQUEST, e).into_response(), + }; + + let config = QrConfig { + level, + version: VersionMode::Auto, + margin: params.margin, + }; + + let qr = match QrCode::encode(¶ms.text, config) { + Ok(q) => q, + Err(e) => return (StatusCode::BAD_REQUEST, e).into_response(), + }; + + let png = match qr.to_png_bytes(params.size) { + Ok(b) => b, + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + }; + + ([(header::CONTENT_TYPE, "image/png")], png).into_response() +} + +#[tokio::main] +async fn main() { + let app = Router::new() + .route("/", get(index)) + .route("/api/qr", get(generate_qr)); + + let addr = "0.0.0.0:3000"; + println!("QRGen Web → http://{}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/web/src/templates/index.html b/web/src/templates/index.html new file mode 100644 index 0000000..a002824 --- /dev/null +++ b/web/src/templates/index.html @@ -0,0 +1,122 @@ + + + + + +QRGen Web + + + +
+

QRGen Web

+ +
+ + + + +
+
+ + + 8px +
+
+ 输入内容生成 QR 码 +
+
+
+ + +
+
+ + + +