From 426a69cd696807c8798a937ff5a970f54f49c5f4 Mon Sep 17 00:00:00 2001 From: "fengchuanhn@gmail.com" Date: Thu, 21 May 2026 02:40:08 +0800 Subject: [PATCH] 11 --- src-tauri/src/commands/software_info.rs | 7 ++ src/api/auth.js | 9 ++ src/api/oemSoftwareInfo.js | 14 ++- src/api/oemUsers.js | 72 +++++++++++ src/components/AppTitlebar.vue | 108 ++++++++++++++-- src/components/oem/OemImageUpload.vue | 101 +++++++++++++++ src/components/oem/OemSubNav.vue | 8 +- src/router/index.js | 3 +- src/stores/auth.js | 21 +++- src/styles/main.css | 46 +++++++ src/views/OemView.vue | 2 +- src/views/oem/OemCardKeysView.vue | 160 +++++++++++++++++++++++- src/views/oem/OemOverviewView.vue | 140 ++++++++++++++++++++- src/views/oem/OemUsersView.vue | 108 ++++++++++++++-- 14 files changed, 754 insertions(+), 45 deletions(-) create mode 100644 src/api/oemUsers.js create mode 100644 src/components/oem/OemImageUpload.vue diff --git a/src-tauri/src/commands/software_info.rs b/src-tauri/src/commands/software_info.rs index 6bcc190..517a851 100644 --- a/src-tauri/src/commands/software_info.rs +++ b/src-tauri/src/commands/software_info.rs @@ -19,6 +19,7 @@ pub struct SoftwareInfo { pub software_name: String, pub logo_url: Option, pub wechat: Option, + pub wechat_qrcode_url: Option, } fn empty_info(oem_id: i64) -> SoftwareInfo { @@ -27,6 +28,7 @@ fn empty_info(oem_id: i64) -> SoftwareInfo { software_name: String::new(), logo_url: None, wechat: None, + wechat_qrcode_url: None, } } @@ -52,6 +54,11 @@ fn parse_software_info(oem_id: i64, value: &Value) -> SoftwareInfo { .and_then(|v| v.as_str()) .filter(|s| !s.is_empty()) .map(str::to_string), + wechat_qrcode_url: data + .get("wechat_qrcode_url") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(str::to_string), } } diff --git a/src/api/auth.js b/src/api/auth.js index ae659ff..b0b358f 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -27,6 +27,15 @@ export function loginApi(payload) { }); } +/** @param {string} token */ +export function fetchMeApi(token) { + return apiRequest("/auth/me", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); +} + /** @param {string} token */ export function logoutApi(token) { return apiRequest("/auth/logout", { diff --git a/src/api/oemSoftwareInfo.js b/src/api/oemSoftwareInfo.js index 2199182..b36a861 100644 --- a/src/api/oemSoftwareInfo.js +++ b/src/api/oemSoftwareInfo.js @@ -1,27 +1,35 @@ import { invoke } from "@tauri-apps/api/core"; /** - * 经 Rust 读取本地 oem 文件中的 ID,并请求公开品牌 API。 * @returns {Promise<{ * oemId: number, * softwareName: string, * logoUrl: string | null, * wechat: string | null, + * wechatQrcode: string | null, * }>} */ export async function fetchSoftwareInfo() { + const empty = { + oemId: 1, + softwareName: "", + logoUrl: null, + wechat: null, + wechatQrcode: null, + }; try { const data = await invoke("get_software_info"); if (!data || typeof data !== "object") { - return { oemId: 1, softwareName: "", logoUrl: null, wechat: null }; + return empty; } return { oemId: data.oemId ?? 1, softwareName: data.softwareName ?? "", logoUrl: data.logoUrl || null, wechat: data.wechat || null, + wechatQrcode: data.wechatQrcodeUrl || null, }; } catch { - return { oemId: 1, softwareName: "", logoUrl: null, wechat: null }; + return empty; } } diff --git a/src/api/oemUsers.js b/src/api/oemUsers.js new file mode 100644 index 0000000..4b48330 --- /dev/null +++ b/src/api/oemUsers.js @@ -0,0 +1,72 @@ +import { apiRequest } from "./client.js"; + +/** + * @param {string} token + * @param {RequestInit} [options] + */ +function oemRequest(token, path, options = {}) { + return apiRequest(path, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); +} + +/** + * @param {string} token + * @param {{ + * page?: number, + * pageSize?: number, + * username?: string, + * oemId?: number | null, + * agentId?: number | null, + * roleId?: number | null, + * }} [params] + */ +export function listOemUsersApi( + 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 oemRequest(token, `/oem/users?${query}`); +} + +/** + * @param {string} token + * @param {object} payload + */ +export function createOemUserApi(token, payload) { + return oemRequest(token, "/oem/users", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +/** + * @param {string} token + * @param {number} userId + * @param {object} payload + */ +export function updateOemUserApi(token, userId, payload) { + return oemRequest(token, `/oem/users/${userId}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +/** @param {string} token @param {number} userId */ +export function deleteOemUserApi(token, userId) { + return oemRequest(token, `/oem/users/${userId}`, { + method: "DELETE", + }); +} diff --git a/src/components/AppTitlebar.vue b/src/components/AppTitlebar.vue index aa0ea33..d068373 100644 --- a/src/components/AppTitlebar.vue +++ b/src/components/AppTitlebar.vue @@ -1,24 +1,46 @@ + + diff --git a/src/components/oem/OemSubNav.vue b/src/components/oem/OemSubNav.vue index 7e0189f..f0b6f78 100644 --- a/src/components/oem/OemSubNav.vue +++ b/src/components/oem/OemSubNav.vue @@ -6,10 +6,10 @@ const route = useRoute(); const menuItems = [ - { 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" }, + { name: "oem-users", label: "用户管理", to: "/oem/users" }, + { name: "oem-card-keys", label: "卡密管理", to: "/oem/card-keys" }, + { name: "oem-desktop-config", label: "桌面配置", to: "/oem/desktop-config" }, + { name: "oem-overview", label: "软件配置", to: "/oem" }, ]; const activeName = computed(() => route.name); diff --git a/src/router/index.js b/src/router/index.js index d441df0..71aa5fa 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -376,7 +376,8 @@ router.beforeEach((to) => { .find((role) => role != null); - + console.log('requiredRole',requiredRole); + console.log('auth.roleId',auth.roleId); if (requiredRole != null && auth.roleId !== requiredRole) { return { name: "home" }; diff --git a/src/stores/auth.js b/src/stores/auth.js index 937f2cf..c87f657 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -1,6 +1,6 @@ import { defineStore } from "pinia"; import { invoke } from "@tauri-apps/api/core"; -import { loginApi, logoutApi, registerApi } from "../api/auth.js"; +import { fetchMeApi, loginApi, logoutApi, registerApi } from "../api/auth.js"; const TOKEN_KEY = "aiclient_token"; const USER_KEY = "aiclient_current_user"; @@ -37,6 +37,7 @@ function applyTokenResponse(store, res, fallbackMessage) { id: data.user.id, username: data.user.username, role_id: data.user.role_id, + vip_end_time: data.user.vip_end_time ?? null, }); return { ok: true, message: res.message || fallbackMessage }; @@ -120,6 +121,24 @@ export const useAuthStore = defineStore("auth", { return applyTokenResponse(this, res, "登录成功"); }, + async refreshProfile() { + if (!this.token) return { ok: false, message: "未登录" }; + const res = await fetchMeApi(this.token); + if (!res.ok || !res.data) { + return { ok: false, message: res.message || "获取用户信息失败" }; + } + const u = res.data; + this.currentUser = { + id: u.id, + username: u.username, + role_id: u.role_id, + vip_end_time: u.vip_end_time ?? null, + }; + localStorage.setItem(USER_KEY, JSON.stringify(this.currentUser)); + syncAuthSessionToRust(this.token, this.currentUser); + return { ok: true, message: "" }; + }, + async logout() { const token = this.token; if (token) { diff --git a/src/styles/main.css b/src/styles/main.css index 7ea1347..9c505a2 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -334,7 +334,23 @@ body { width: 100%; } +.app-titlebar__logo-wrap { + display: flex; + align-items: center; + justify-content: center; + align-self: center; + height: 100%; + flex-shrink: 0; +} + .app-titlebar__logo { + display: block; + max-height: 2rem; + max-width: 3.5rem; + width: auto; + height: auto; + object-fit: contain; + object-position: center; border-radius: 4px; } @@ -344,6 +360,36 @@ body { .app-titlebar__wechat { max-width: 12rem; + border: none; + background: transparent; + padding: 0; + font: inherit; + text-align: left; +} + +.app-titlebar__wechat--clickable { + cursor: pointer; + transition: color 0.15s; +} + +.app-titlebar__wechat--clickable:hover { + color: #e2e8f0; +} + +.app-titlebar__user { + max-width: 28rem; +} + +.app-titlebar__username { + max-width: 8rem; +} + +.app-titlebar__vip { + white-space: nowrap; +} + +.app-titlebar__logout { + flex-shrink: 0; } .app-titlebar__controls { diff --git a/src/views/OemView.vue b/src/views/OemView.vue index ae9b81f..25b05ba 100644 --- a/src/views/OemView.vue +++ b/src/views/OemView.vue @@ -4,7 +4,7 @@ import OemSubNav from "../components/oem/OemSubNav.vue"; + + + + + + + + + + + +