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, /// fmt=svg 返回 SVG,否则返回 PNG #[serde(default)] fmt: String, } 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 或 SVG 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(), }; if params.fmt == "svg" { let svg = qr.to_svg(); ([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response() } else { match qr.to_png_bytes(params.size) { Ok(b) => ([(header::CONTENT_TYPE, "image/png")], b).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).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(); }