From 54f080fbc11dfa58bd0160bb0dbad3d79bfc0373 Mon Sep 17 00:00:00 2001 From: "fengchuanhn@gmail.com" Date: Thu, 21 May 2026 01:47:16 +0800 Subject: [PATCH] 11 --- src-tauri/capabilities/default.json | 4 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/software_info.rs | 71 ++++++++++ src-tauri/src/lib.rs | 10 ++ src-tauri/src/oem_context.rs | 6 + src-tauri/src/oem_id.rs | 67 +++++++++ src/api/adminAppBranding.js | 113 +++++++++++++++ src/api/adminCardKeys.js | 18 ++- src/api/adminUsers.js | 19 ++- src/api/oemSoftwareInfo.js | 27 ++++ src/components/AppTitlebar.vue | 152 +++++++++++++++++++- src/components/admin/AdminImageUpload.vue | 101 ++++++++++++++ src/components/admin/AdminSubNav.vue | 5 +- src/components/oem/OemSubNav.vue | 5 +- src/main.js | 4 + src/router/index.js | 8 +- src/styles/main.css | 128 +++++++++++++++++ src/views/admin/AdminCardKeysView.vue | 160 +++++++++++++++++++++- src/views/admin/AdminOverviewView.vue | 140 ++++++++++++++++++- src/views/admin/AdminUsersView.vue | 95 ++++++++++++- src/views/oem/OemOverviewView.vue | 4 +- vite.config.js | 4 + 22 files changed, 1106 insertions(+), 36 deletions(-) create mode 100644 src-tauri/src/commands/software_info.rs create mode 100644 src-tauri/src/oem_context.rs create mode 100644 src-tauri/src/oem_id.rs create mode 100644 src/api/adminAppBranding.js create mode 100644 src/api/oemSoftwareInfo.js create mode 100644 src/components/admin/AdminImageUpload.vue diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 117aba4..569e9a3 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,10 @@ "core:default", "core:window:default", "core:window:allow-start-dragging", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-is-maximized", "core:event:default", "core:event:allow-listen", "core:event:allow-unlisten", diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index cf4ad8f..4810e8c 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -8,3 +8,4 @@ pub mod fs_util; pub mod nodejs; pub mod publish; pub mod quickjs; +pub mod software_info; diff --git a/src-tauri/src/commands/software_info.rs b/src-tauri/src/commands/software_info.rs new file mode 100644 index 0000000..f322adc --- /dev/null +++ b/src-tauri/src/commands/software_info.rs @@ -0,0 +1,71 @@ +//! 按本地 oem 文件中的 ID 拉取公开品牌信息。 + +use std::collections::HashMap; + +use serde::Serialize; +use serde_json::Value; +use tauri::State; + +use crate::api_client; +use crate::oem_context::OemContext; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SoftwareInfo { + pub oem_id: i64, + pub software_name: String, + pub logo_url: Option, + pub wechat: Option, +} + +fn empty_info(oem_id: i64) -> SoftwareInfo { + SoftwareInfo { + oem_id, + software_name: String::new(), + logo_url: None, + wechat: None, + } +} + +fn parse_software_info(oem_id: i64, value: &Value) -> SoftwareInfo { + let data = value.get("data").unwrap_or(value); + SoftwareInfo { + oem_id: data + .get("oem_id") + .and_then(|v| v.as_i64()) + .unwrap_or(oem_id), + software_name: data + .get("software_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + logo_url: data + .get("logo_url") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(str::to_string), + wechat: data + .get("wechat") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(str::to_string), + } +} + +#[tauri::command] +pub async fn get_software_info(ctx: State<'_, OemContext>) -> Result { + let oem_id = ctx.oem_id; + let path = format!("/public/oem-branding/{oem_id}"); + let (status, value) = + api_client::request_json("GET", &path, None, HashMap::new()).await?; + + if status >= 400 { + return Ok(empty_info(oem_id)); + } + + if value.get("ok").and_then(|v| v.as_bool()) == Some(false) { + return Ok(empty_info(oem_id)); + } + + Ok(parse_software_info(oem_id, &value)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 25e8bb2..c292eac 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,8 @@ pub mod ffmpeg; pub mod http; pub mod js_runtime; pub mod nodejs; +pub mod oem_context; +pub mod oem_id; pub mod oss; pub mod publish_db; pub mod publish_store; @@ -36,6 +38,13 @@ pub fn run() { .manage(publish_store::PublishStore::new()) .setup(|app| { let handle = app.handle().clone(); + let oem_id = handle + .path() + .app_data_dir() + .map(|dir| oem_id::read_oem_id_file(&dir.join(oem_id::OEM_FILE_NAME))) + .unwrap_or(oem_id::DEFAULT_OEM_ID); + app.manage(oem_context::OemContext { oem_id }); + tauri::async_runtime::block_on(async move { let app_config = handle.state::(); let avatar_store = handle.state::(); @@ -68,6 +77,7 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ commands::api::api_request, + commands::software_info::get_software_info, commands::auth::sync_auth_session, commands::app_config::get_app_config, commands::app_config::get_app_config_last_message, diff --git a/src-tauri/src/oem_context.rs b/src-tauri/src/oem_context.rs new file mode 100644 index 0000000..e4d7856 --- /dev/null +++ b/src-tauri/src/oem_context.rs @@ -0,0 +1,6 @@ +//! 启动时解析的 OEM 上下文。 + +#[derive(Debug, Clone)] +pub struct OemContext { + pub oem_id: i64, +} diff --git a/src-tauri/src/oem_id.rs b/src-tauri/src/oem_id.rs new file mode 100644 index 0000000..93f224a --- /dev/null +++ b/src-tauri/src/oem_id.rs @@ -0,0 +1,67 @@ +//! 从应用数据目录的 `oem` 文件读取 OEM 编号。 + +use std::path::Path; + +use serde_json::Value; + +pub const DEFAULT_OEM_ID: i64 = 1; + +/// 应用数据目录下的 OEM 配置文件名。 +pub const OEM_FILE_NAME: &str = "oem"; + +pub fn read_oem_id_file(path: &Path) -> i64 { + let content = match std::fs::read_to_string(path) { + Ok(s) => s, + Err(_) => return DEFAULT_OEM_ID, + }; + parse_oem_id_content(&content).unwrap_or(DEFAULT_OEM_ID) +} + +fn parse_oem_id_content(content: &str) -> Option { + let trimmed = content.trim(); + if trimmed.is_empty() { + return None; + } + + if let Ok(value) = serde_json::from_str::(trimmed) { + if let Some(id) = value.get("oem_id").and_then(|v| v.as_i64()) { + if id >= 1 { + return Some(id); + } + } + if let Some(id) = value.as_i64() { + if id >= 1 { + return Some(id); + } + } + } + + let first_line = trimmed.lines().next()?.trim(); + let id: i64 = first_line.parse().ok()?; + if id >= 1 { + Some(id) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_integer_line() { + assert_eq!(parse_oem_id_content("2\n"), Some(2)); + } + + #[test] + fn parse_json_object() { + assert_eq!(parse_oem_id_content(r#"{"oem_id":3}"#), Some(3)); + } + + #[test] + fn invalid_defaults_none() { + assert_eq!(parse_oem_id_content("abc"), None); + assert_eq!(parse_oem_id_content(""), None); + } +} diff --git a/src/api/adminAppBranding.js b/src/api/adminAppBranding.js new file mode 100644 index 0000000..41b0b1f --- /dev/null +++ b/src/api/adminAppBranding.js @@ -0,0 +1,113 @@ +import { apiRequest } from "./client.js"; + +/** 品牌静态资源基址(与 pythonbackend 根目录 images 挂载的 /images 一致) */ +const MEDIA_BASE = + import.meta.env.VITE_MEDIA_BASE_URL || "http://127.0.0.1:8001"; + +/** + * @param {string} token + * @param {RequestInit} [options] + */ +function adminRequest(token, path, options = {}) { + return apiRequest(path, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); +} + +function emptyBrandingForm() { + return { + softwareName: "", + logo: "", + contactWechat: "", + contactQq: "", + wechatQr: "", + qqQr: "", + }; +} + +/** + * 库内路径 images/xxx → 可访问 URL(预览用) + * @param {string | null | undefined} path + */ +export function brandingImageUrl(path) { + if (!path?.trim()) return ""; + const value = path.trim(); + if (value.startsWith("data:image/") || value.startsWith("http")) return value; + const normalized = value.replace(/\\/g, "/").replace(/^\//, ""); + if (normalized.startsWith("images/")) { + return `${MEDIA_BASE}/${normalized}`; + } + return `${MEDIA_BASE}/images/${normalized}`; +} + +/** 提交前:新上传保留 data URL;已有文件只提交 images/ 相对路径 */ +function toStoredImageValue(value) { + if (!value?.trim()) return null; + const v = value.trim(); + if (v.startsWith("data:image/")) return v; + const match = v.match(/\/images\/(.+?)(?:\?.*)?$/); + if (match) return `images/${match[1]}`; + if (v.startsWith("images/")) return v; + return v; +} + +/** API 载荷 → 表单 */ +function toForm(data) { + if (!data) return emptyBrandingForm(); + return { + softwareName: data.software_name ?? "", + logo: brandingImageUrl(data.logo_path), + contactWechat: data.wechat ?? "", + contactQq: data.qq ?? "", + wechatQr: brandingImageUrl(data.wechat_qrcode), + qqQr: brandingImageUrl(data.qq_qrcode), + }; +} + +/** 表单 → API 载荷 */ +function toPayload(form) { + return { + software_name: String(form.softwareName ?? "").trim(), + logo_path: toStoredImageValue(form.logo), + wechat: form.contactWechat?.trim() || null, + qq: form.contactQq?.trim() || null, + wechat_qrcode: toStoredImageValue(form.wechatQr), + qq_qrcode: toStoredImageValue(form.qqQr), + }; +} + +/** + * 加载默认 OEM(id=1)软件配置 + * @param {string} token + */ +export async function loadAdminAppBrandingApi(token) { + const res = await adminRequest(token, "/admin/oem-branding"); + if (!res.ok) { + return { ok: false, message: res.message || "加载软件配置失败" }; + } + return { ok: true, data: { form: toForm(res.data), oemId: res.data?.id ?? 1 } }; +} + +/** + * 保存默认 OEM(id=1)软件配置 + * @param {string} token + * @param {ReturnType} form + */ +export async function saveAdminAppBrandingApi(token, form) { + const res = await adminRequest(token, "/admin/oem-branding", { + method: "PATCH", + body: JSON.stringify(toPayload(form)), + }); + if (!res.ok) { + return { ok: false, message: res.message || "保存软件配置失败" }; + } + return { + ok: true, + message: res.message || "软件配置已保存", + data: { form: toForm(res.data), oemId: res.data?.id ?? 1 }, + }; +} diff --git a/src/api/adminCardKeys.js b/src/api/adminCardKeys.js index b149a7a..b81628c 100644 --- a/src/api/adminCardKeys.js +++ b/src/api/adminCardKeys.js @@ -16,14 +16,28 @@ function adminRequest(token, path, options = {}) { /** * @param {string} token - * @param {{ page?: number, pageSize?: number, status?: string }} [params] + * @param {{ + * page?: number, + * pageSize?: number, + * status?: string, + * username?: string, + * oemId?: number | null, + * agentId?: number | null, + * }} [params] */ -export function listAdminCardKeysApi(token, { page = 1, pageSize = 20, status = "all" } = {}) { +export function listAdminCardKeysApi( + token, + { page = 1, pageSize = 20, status = "all", username, oemId, agentId } = {}, +) { const query = new URLSearchParams({ page: String(page), page_size: String(pageSize), status, }); + const trimmed = username?.trim(); + if (trimmed) query.set("username", trimmed); + if (oemId != null) query.set("oem_id", String(oemId)); + if (agentId != null) query.set("agent_id", String(agentId)); return adminRequest(token, `/admin/card-keys?${query}`); } diff --git a/src/api/adminUsers.js b/src/api/adminUsers.js index e576969..df3bc2e 100644 --- a/src/api/adminUsers.js +++ b/src/api/adminUsers.js @@ -16,13 +16,28 @@ function adminRequest(token, path, options = {}) { /** * @param {string} token - * @param {{ page?: number, pageSize?: number }} [params] + * @param {{ + * page?: number, + * pageSize?: number, + * username?: string, + * oemId?: number | null, + * agentId?: number | null, + * roleId?: number | null, + * }} [params] */ -export function listAdminUsersApi(token, { page = 1, pageSize = 20 } = {}) { +export function listAdminUsersApi( + token, + { page = 1, pageSize = 20, username, oemId, agentId, roleId } = {}, +) { const query = new URLSearchParams({ page: String(page), page_size: String(pageSize), }); + const trimmed = username?.trim(); + if (trimmed) query.set("username", trimmed); + if (oemId != null) query.set("oem_id", String(oemId)); + if (agentId != null) query.set("agent_id", String(agentId)); + if (roleId != null) query.set("role_id", String(roleId)); return adminRequest(token, `/admin/users?${query}`); } diff --git a/src/api/oemSoftwareInfo.js b/src/api/oemSoftwareInfo.js new file mode 100644 index 0000000..2199182 --- /dev/null +++ b/src/api/oemSoftwareInfo.js @@ -0,0 +1,27 @@ +import { invoke } from "@tauri-apps/api/core"; + +/** + * 经 Rust 读取本地 oem 文件中的 ID,并请求公开品牌 API。 + * @returns {Promise<{ + * oemId: number, + * softwareName: string, + * logoUrl: string | null, + * wechat: string | null, + * }>} + */ +export async function fetchSoftwareInfo() { + try { + const data = await invoke("get_software_info"); + if (!data || typeof data !== "object") { + return { oemId: 1, softwareName: "", logoUrl: null, wechat: null }; + } + return { + oemId: data.oemId ?? 1, + softwareName: data.softwareName ?? "", + logoUrl: data.logoUrl || null, + wechat: data.wechat || null, + }; + } catch { + return { oemId: 1, softwareName: "", logoUrl: null, wechat: null }; + } +} diff --git a/src/components/AppTitlebar.vue b/src/components/AppTitlebar.vue index 4c6eaae..82a6843 100644 --- a/src/components/AppTitlebar.vue +++ b/src/components/AppTitlebar.vue @@ -1,10 +1,148 @@ + + diff --git a/src/components/admin/AdminImageUpload.vue b/src/components/admin/AdminImageUpload.vue new file mode 100644 index 0000000..7339f74 --- /dev/null +++ b/src/components/admin/AdminImageUpload.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/components/admin/AdminSubNav.vue b/src/components/admin/AdminSubNav.vue index f4b096f..75f91ee 100644 --- a/src/components/admin/AdminSubNav.vue +++ b/src/components/admin/AdminSubNav.vue @@ -5,10 +5,11 @@ import { useRoute } from "vue-router"; const route = useRoute(); const menuItems = [ - { name: "admin-overview", label: "概览", to: "/admin" }, + { name: "admin-users", label: "用户管理", to: "/admin/users" }, { name: "admin-card-keys", label: "卡密管理", to: "/admin/card-keys" }, { name: "admin-desktop-config", label: "桌面配置", to: "/admin/desktop-config" }, + { name: "admin-overview", label: "软件配置", to: "/admin" }, ]; const activeName = computed(() => route.name); @@ -16,7 +17,7 @@ const activeName = computed(() => route.name);