feat: 新增自定义窗口背景功能
This commit is contained in:
@@ -4,6 +4,28 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.5.0(2026-10-28)
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- **跟随系统主题**:「设置 - 外观」新增「跟随系统」,可随操作系统的明暗主题自动切换;也可继续手动选择浅色或深色主题。
|
||||||
|
- **自定义窗口背景**:可在「设置 - 外观」中选择或更换自己的背景图片,支持 PNG、JPG 和 WebP,单张图片不超过 15 MB。图片会保存到应用数据目录,重新打开软件后仍可使用。
|
||||||
|
- **窗口背景遮罩**:在窗口背景上叠加随明暗主题变化的遮罩,并可在外观设置中单独调整透明度,改善背景与顶部栏操作图标之间的对比度。
|
||||||
|
|
||||||
|
### 🎨 优化
|
||||||
|
|
||||||
|
- **主题背景**:默认背景颜色改为适配明暗主题的纯色,并调整内置背景图片与明暗主题下的整体观感。
|
||||||
|
- **折叠侧栏资源占用**:菜单栏折叠后直接显示 CPU、内存百分比和运行状态,无需悬停查看。
|
||||||
|
- **系统资源数据复用**:仪表盘与侧栏共享 CPU、内存占用数据缓存,减少进入仪表盘后等待数据加载的时间。
|
||||||
|
|
||||||
|
### 📁 数据与日志
|
||||||
|
|
||||||
|
- **Frpc 运行日志持久化**:运行日志按日期追加保存到安装目录的 `logs/frpc-YYYY-MM-DD.log`,包含 Frpc 输出及启动、停止和退出等状态记录。
|
||||||
|
|
||||||
|
### 🧱 工程
|
||||||
|
|
||||||
|
- 应用版本升级至 **v1.5.0**。
|
||||||
|
|
||||||
## v1.4.1(2026-10-26)
|
## v1.4.1(2026-10-26)
|
||||||
|
|
||||||
### 🎨 优化
|
### 🎨 优化
|
||||||
|
|||||||
Generated
+7
@@ -1843,6 +1843,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -4595,6 +4601,7 @@ dependencies = [
|
|||||||
"gtk",
|
"gtk",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"http",
|
"http",
|
||||||
|
"http-range",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri-helper = "0.2.1"
|
tauri-helper = "0.2.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.12.3", features = ["tray-icon"] }
|
tauri = { version = "2.12.3", features = ["protocol-asset", "tray-icon"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use tauri::{AppHandle, State};
|
||||||
|
use tauri_helper::auto_collect_command;
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
|
||||||
|
use crate::models::ApiResponse;
|
||||||
|
use crate::services::background::{find_custom_background, import_custom_background};
|
||||||
|
use crate::services::storage::StoragePaths;
|
||||||
|
|
||||||
|
/// 打开原生文件选择器并将图片复制到应用数据目录。
|
||||||
|
#[tauri::command]
|
||||||
|
#[auto_collect_command]
|
||||||
|
pub async fn choose_custom_background(
|
||||||
|
app: AppHandle,
|
||||||
|
paths: State<'_, StoragePaths>,
|
||||||
|
) -> Result<ApiResponse<Option<String>>, String> {
|
||||||
|
let picked = app
|
||||||
|
.dialog()
|
||||||
|
.file()
|
||||||
|
.add_filter("图片", &["png", "jpg", "jpeg", "webp"])
|
||||||
|
.set_title("选择背景图片")
|
||||||
|
.blocking_pick_file();
|
||||||
|
let Some(file) = picked else {
|
||||||
|
return Ok(ApiResponse::success(None));
|
||||||
|
};
|
||||||
|
let source = file.into_path().map_err(|e| e.to_string())?;
|
||||||
|
let saved = import_custom_background(&paths, &source)?;
|
||||||
|
Ok(ApiResponse::success(Some(
|
||||||
|
saved.to_string_lossy().into_owned(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取已保存的自定义背景路径,图片内容通过受限的 asset 协议读取。
|
||||||
|
#[tauri::command]
|
||||||
|
#[auto_collect_command]
|
||||||
|
pub async fn get_custom_background(
|
||||||
|
paths: State<'_, StoragePaths>,
|
||||||
|
) -> Result<ApiResponse<Option<String>>, String> {
|
||||||
|
Ok(ApiResponse::success(
|
||||||
|
find_custom_background(&paths).map(|path| path.to_string_lossy().into_owned()),
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod background;
|
||||||
pub mod frp;
|
pub mod frp;
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod services;
|
|||||||
use tauri::{Manager, RunEvent};
|
use tauri::{Manager, RunEvent};
|
||||||
use tauri_plugin_autostart::ManagerExt as _;
|
use tauri_plugin_autostart::ManagerExt as _;
|
||||||
|
|
||||||
|
use crate::commands::background::{choose_custom_background, get_custom_background};
|
||||||
use crate::commands::frp::{
|
use crate::commands::frp::{
|
||||||
delete_frp, download_frp, get_frp_downloads, get_frp_releases, import_frp,
|
delete_frp, download_frp, get_frp_downloads, get_frp_releases, import_frp,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::services::storage::StoragePaths;
|
||||||
|
|
||||||
|
const MAX_BACKGROUND_SIZE: u64 = 15 * 1024 * 1024;
|
||||||
|
|
||||||
|
pub fn import_custom_background(paths: &StoragePaths, source: &Path) -> Result<PathBuf, String> {
|
||||||
|
let size = fs::metadata(source)
|
||||||
|
.map_err(|e| format!("读取图片信息失败:{e}"))?
|
||||||
|
.len();
|
||||||
|
if size == 0 || size > MAX_BACKGROUND_SIZE {
|
||||||
|
return Err("图片大小必须在 1 字节到 15 MB 之间".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = fs::read(source).map_err(|e| format!("读取背景图片失败:{e}"))?;
|
||||||
|
if bytes.is_empty() || bytes.len() as u64 > MAX_BACKGROUND_SIZE {
|
||||||
|
return Err("图片大小必须在 1 字节到 15 MB 之间".into());
|
||||||
|
}
|
||||||
|
let extension = detect_extension(&bytes).ok_or("仅支持 PNG、JPG 和 WebP 图片")?;
|
||||||
|
let directory = paths.data().join("backgrounds");
|
||||||
|
fs::create_dir_all(&directory).map_err(|e| format!("创建背景图片目录失败:{e}"))?;
|
||||||
|
|
||||||
|
let destination = directory.join(format!("custom.{extension}"));
|
||||||
|
let temporary = directory.join(format!("custom.{extension}.tmp"));
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&temporary)
|
||||||
|
.map_err(|e| format!("创建背景图片文件失败:{e}"))?;
|
||||||
|
if let Err(error) = file.write_all(&bytes).and_then(|()| file.sync_all()) {
|
||||||
|
drop(file);
|
||||||
|
let _ = fs::remove_file(&temporary);
|
||||||
|
return Err(format!("保存背景图片失败:{error}"));
|
||||||
|
}
|
||||||
|
drop(file);
|
||||||
|
fs::rename(&temporary, &destination).map_err(|e| {
|
||||||
|
let _ = fs::remove_file(&temporary);
|
||||||
|
format!("更新背景图片失败:{e}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for stale_extension in ["png", "jpg", "webp"] {
|
||||||
|
if stale_extension != extension {
|
||||||
|
let stale = directory.join(format!("custom.{stale_extension}"));
|
||||||
|
let _ = fs::remove_file(stale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_custom_background(paths: &StoragePaths) -> Option<PathBuf> {
|
||||||
|
["png", "jpg", "webp"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|extension| {
|
||||||
|
paths
|
||||||
|
.data()
|
||||||
|
.join("backgrounds")
|
||||||
|
.join(format!("custom.{extension}"))
|
||||||
|
})
|
||||||
|
.filter(|path| path.is_file())
|
||||||
|
.max_by_key(|path| {
|
||||||
|
fs::metadata(path)
|
||||||
|
.and_then(|metadata| metadata.modified())
|
||||||
|
.unwrap_or(std::time::UNIX_EPOCH)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_extension(bytes: &[u8]) -> Option<&'static str> {
|
||||||
|
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
|
||||||
|
Some("png")
|
||||||
|
} else if bytes.starts_with(b"\xff\xd8\xff") {
|
||||||
|
Some("jpg")
|
||||||
|
} else if bytes.len() >= 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
|
||||||
|
Some("webp")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn imports_supported_image_and_replaces_previous_format() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"frpcpilot-background-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
fs::create_dir_all(&root).unwrap();
|
||||||
|
let paths = StoragePaths { root: root.clone() };
|
||||||
|
let source = root.join("source-image.bin");
|
||||||
|
let png = b"\x89PNG\r\n\x1a\nimage-data";
|
||||||
|
fs::write(&source, png).unwrap();
|
||||||
|
|
||||||
|
let saved = import_custom_background(&paths, &source).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(saved, paths.data().join("backgrounds/custom.png"));
|
||||||
|
assert_eq!(fs::read(&saved).unwrap(), png);
|
||||||
|
assert_eq!(find_custom_background(&paths), Some(saved.clone()));
|
||||||
|
|
||||||
|
let jpeg = b"\xff\xd8\xffimage-data";
|
||||||
|
fs::write(&source, jpeg).unwrap();
|
||||||
|
let replaced = import_custom_background(&paths, &source).unwrap();
|
||||||
|
assert_eq!(replaced, paths.data().join("backgrounds/custom.jpg"));
|
||||||
|
assert!(!saved.exists());
|
||||||
|
assert_eq!(fs::read(&replaced).unwrap(), jpeg);
|
||||||
|
assert_eq!(find_custom_background(&paths), Some(replaced));
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_image_files() {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"frpcpilot-background-invalid-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
fs::create_dir_all(&root).unwrap();
|
||||||
|
let paths = StoragePaths { root: root.clone() };
|
||||||
|
let source = root.join("not-an-image");
|
||||||
|
fs::write(&source, b"not an image").unwrap();
|
||||||
|
|
||||||
|
assert!(import_custom_background(&paths, &source).is_err());
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod background;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
|
|||||||
@@ -22,7 +22,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null,
|
||||||
|
"assetProtocol": {
|
||||||
|
"enable": true,
|
||||||
|
"scope": ["$APPDATA/backgrounds/**"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
|
import type { ApiResponse } from "@/types/api"
|
||||||
|
|
||||||
|
async function call<T>(command: string): Promise<T> {
|
||||||
|
const response = await invoke<ApiResponse<T>>(command)
|
||||||
|
if (!response?.success) {
|
||||||
|
throw new Error(response?.msg || "操作失败")
|
||||||
|
}
|
||||||
|
return response.data as T
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 让用户选择并持久化一张自定义背景图片,取消时返回 null。 */
|
||||||
|
export function chooseCustomBackground() {
|
||||||
|
return call<string | null>("choose_custom_background")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已保存的自定义背景图片路径。 */
|
||||||
|
export function getCustomBackground() {
|
||||||
|
return call<string | null>("get_custom_background")
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import bg8 from "@/assets/images/bg-8.webp"
|
|||||||
import bg9 from "@/assets/images/bg-9.webp"
|
import bg9 from "@/assets/images/bg-9.webp"
|
||||||
|
|
||||||
/** 窗口背景图片标识 */
|
/** 窗口背景图片标识 */
|
||||||
export type BackgroundKey = "none" | "bg-1" | "bg-2" | "bg-3" | "bg-4" | "bg-5" | "bg-6" | "bg-7" | "bg-8" | "bg-9"
|
export type BackgroundKey = "none" | "custom" | "bg-1" | "bg-2" | "bg-3" | "bg-4" | "bg-5" | "bg-6" | "bg-7" | "bg-8" | "bg-9"
|
||||||
|
|
||||||
export interface BackgroundOption {
|
export interface BackgroundOption {
|
||||||
/** 唯一标识 */
|
/** 唯一标识 */
|
||||||
@@ -38,5 +38,6 @@ export const DEFAULT_BACKGROUND: BackgroundKey = "none"
|
|||||||
|
|
||||||
/** 根据标识取图片地址 */
|
/** 根据标识取图片地址 */
|
||||||
export function getBackgroundUrl(key: BackgroundKey): string {
|
export function getBackgroundUrl(key: BackgroundKey): string {
|
||||||
|
if (key === "custom") return ""
|
||||||
return backgroundOptions.find((item) => item.key === key)?.url ?? ""
|
return backgroundOptions.find((item) => item.key === key)?.url ?? ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,9 @@
|
|||||||
"system": "System",
|
"system": "System",
|
||||||
"background": "Background image",
|
"background": "Background image",
|
||||||
"backgroundTip": "Pick an image as the window background. It keeps its original aspect ratio and is never stretched.",
|
"backgroundTip": "Pick an image as the window background. It keeps its original aspect ratio and is never stretched.",
|
||||||
|
"customBackground": "Custom",
|
||||||
|
"chooseCustomBackground": "Choose image",
|
||||||
|
"changeCustomBackground": "Change custom background",
|
||||||
"backgroundOverlay": "Window background overlay",
|
"backgroundOverlay": "Window background overlay",
|
||||||
"backgroundOverlayTip": "Apply a theme-aware overlay across the window to improve contrast with title bar icons.",
|
"backgroundOverlayTip": "Apply a theme-aware overlay across the window to improve contrast with title bar icons.",
|
||||||
"overlayTransparency": "Overlay transparency",
|
"overlayTransparency": "Overlay transparency",
|
||||||
|
|||||||
@@ -127,6 +127,9 @@
|
|||||||
"system": "跟随系统",
|
"system": "跟随系统",
|
||||||
"background": "背景图片",
|
"background": "背景图片",
|
||||||
"backgroundTip": "选择一张图片作为窗口背景,图片保持原始比例自适应,不会变形拉伸",
|
"backgroundTip": "选择一张图片作为窗口背景,图片保持原始比例自适应,不会变形拉伸",
|
||||||
|
"customBackground": "自定义",
|
||||||
|
"chooseCustomBackground": "选择图片",
|
||||||
|
"changeCustomBackground": "更换自定义背景",
|
||||||
"backgroundOverlay": "窗口背景遮罩",
|
"backgroundOverlay": "窗口背景遮罩",
|
||||||
"backgroundOverlayTip": "为整个窗口叠加随主题变化的遮罩,改善背景与顶部栏图标的对比度",
|
"backgroundOverlayTip": "为整个窗口叠加随主题变化的遮罩,改善背景与顶部栏图标的对比度",
|
||||||
"overlayTransparency": "遮罩透明度",
|
"overlayTransparency": "遮罩透明度",
|
||||||
|
|||||||
+34
-1
@@ -1,5 +1,7 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
|
import { convertFileSrc } from "@tauri-apps/api/core"
|
||||||
import { preferences } from "@/utils/preferences"
|
import { preferences } from "@/utils/preferences"
|
||||||
|
import { chooseCustomBackground as chooseCustomBackgroundFile, getCustomBackground } from "@/api/background"
|
||||||
import zhCN from "ant-design-vue/es/locale/zh_CN"
|
import zhCN from "ant-design-vue/es/locale/zh_CN"
|
||||||
import enUS from "ant-design-vue/es/locale/en_US"
|
import enUS from "ant-design-vue/es/locale/en_US"
|
||||||
import { setI18nLanguage, type Language } from "@/i18n"
|
import { setI18nLanguage, type Language } from "@/i18n"
|
||||||
@@ -8,6 +10,7 @@ import {
|
|||||||
getBackgroundUrl,
|
getBackgroundUrl,
|
||||||
type BackgroundKey,
|
type BackgroundKey,
|
||||||
} from "@/config/backgrounds"
|
} from "@/config/backgrounds"
|
||||||
|
import { isTauri } from "@/utils/window"
|
||||||
|
|
||||||
/** Frpc 运行状态 */
|
/** Frpc 运行状态 */
|
||||||
export type RunStatus = "running" | "error" | "stopped"
|
export type RunStatus = "running" | "error" | "stopped"
|
||||||
@@ -46,7 +49,35 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
|
|
||||||
// 窗口背景图片(持久化)
|
// 窗口背景图片(持久化)
|
||||||
const background = useStorage<BackgroundKey>("frpcpilot:background", DEFAULT_BACKGROUND, preferences)
|
const background = useStorage<BackgroundKey>("frpcpilot:background", DEFAULT_BACKGROUND, preferences)
|
||||||
const backgroundUrl = computed(() => getBackgroundUrl(background.value))
|
const customBackgroundPath = ref("")
|
||||||
|
const customBackgroundRevision = ref(0)
|
||||||
|
const customBackgroundUrl = computed(() => {
|
||||||
|
if (!customBackgroundPath.value) return ""
|
||||||
|
return `${convertFileSrc(customBackgroundPath.value)}?v=${customBackgroundRevision.value}`
|
||||||
|
})
|
||||||
|
const backgroundUrl = computed(() => background.value === "custom" ? customBackgroundUrl.value : getBackgroundUrl(background.value))
|
||||||
|
|
||||||
|
async function loadCustomBackground() {
|
||||||
|
if (!isTauri()) return
|
||||||
|
try {
|
||||||
|
customBackgroundPath.value = (await getCustomBackground()) ?? ""
|
||||||
|
customBackgroundRevision.value = Date.now()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load custom background", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseCustomBackground() {
|
||||||
|
if (!isTauri()) throw new Error("自定义背景图片仅在桌面应用中可用")
|
||||||
|
const path = await chooseCustomBackgroundFile()
|
||||||
|
if (!path) return false
|
||||||
|
customBackgroundPath.value = path
|
||||||
|
customBackgroundRevision.value = Date.now()
|
||||||
|
background.value = "custom"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadCustomBackground()
|
||||||
|
|
||||||
// 把背景图同步到 <html>(整窗背景,避免元素高度误差造成的上下留白)
|
// 把背景图同步到 <html>(整窗背景,避免元素高度误差造成的上下留白)
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@@ -118,6 +149,8 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
toggleDark,
|
toggleDark,
|
||||||
background,
|
background,
|
||||||
backgroundUrl,
|
backgroundUrl,
|
||||||
|
customBackgroundUrl,
|
||||||
|
chooseCustomBackground,
|
||||||
cardTransparency,
|
cardTransparency,
|
||||||
modalTransparency,
|
modalTransparency,
|
||||||
backgroundOverlayTransparency,
|
backgroundOverlayTransparency,
|
||||||
|
|||||||
@@ -47,6 +47,44 @@
|
|||||||
<CheckCircleFilled v-if="appStore.background === bg.key" class="shrink-0 text-[#6366f1]" />
|
<CheckCircleFilled v-if="appStore.background === bg.key" class="shrink-0 text-[#6366f1]" />
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
|
<a-card
|
||||||
|
hoverable
|
||||||
|
size="small"
|
||||||
|
class="bg-card overflow-hidden"
|
||||||
|
:class="{ 'bg-card--active': appStore.background === 'custom' }"
|
||||||
|
:body-style="{ padding: '8px 10px' }"
|
||||||
|
@click="selectCustomBackground"
|
||||||
|
>
|
||||||
|
<template #cover>
|
||||||
|
<div
|
||||||
|
v-if="appStore.customBackgroundUrl"
|
||||||
|
class="bg-thumb"
|
||||||
|
:style="{ backgroundImage: `url('${appStore.customBackgroundUrl}')` }"
|
||||||
|
/>
|
||||||
|
<div v-else class="bg-thumb bg-thumb--custom-empty">
|
||||||
|
<a-button type="link" :icon="h(PictureOutlined)" @click.stop="chooseCustomBackground">
|
||||||
|
{{ t("settings.appearance.chooseCustomBackground") }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-1 text-xs">
|
||||||
|
<span class="truncate">{{ t("settings.appearance.customBackground") }}</span>
|
||||||
|
<a-space :size="2">
|
||||||
|
<a-tooltip :title="t('settings.appearance.changeCustomBackground')">
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:icon="h(UploadOutlined)"
|
||||||
|
:aria-label="t('settings.appearance.changeCustomBackground')"
|
||||||
|
@click.stop="chooseCustomBackground"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
<CheckCircleFilled v-if="appStore.background === 'custom'" class="shrink-0 text-[#6366f1]" />
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -85,7 +123,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from "vue"
|
import { h } from "vue"
|
||||||
import { BulbFilled, BulbOutlined, CheckCircleFilled, DesktopOutlined } from "@ant-design/icons-vue"
|
import { BulbFilled, BulbOutlined, CheckCircleFilled, DesktopOutlined, PictureOutlined, UploadOutlined } from "@ant-design/icons-vue"
|
||||||
|
import { message } from "ant-design-vue"
|
||||||
import { useI18n } from "vue-i18n"
|
import { useI18n } from "vue-i18n"
|
||||||
import { backgroundOptions, type BackgroundKey } from "@/config/backgrounds"
|
import { backgroundOptions, type BackgroundKey } from "@/config/backgrounds"
|
||||||
import { useAppStore } from "@/stores/app"
|
import { useAppStore } from "@/stores/app"
|
||||||
@@ -120,6 +159,22 @@ const themeOptions = computed(() => [
|
|||||||
function selectBackground(key: BackgroundKey) {
|
function selectBackground(key: BackgroundKey) {
|
||||||
appStore.setBackground(key)
|
appStore.setBackground(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectCustomBackground() {
|
||||||
|
if (appStore.customBackgroundUrl) {
|
||||||
|
appStore.setBackground("custom")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await chooseCustomBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseCustomBackground() {
|
||||||
|
try {
|
||||||
|
await appStore.chooseCustomBackground()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : String(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -150,6 +205,17 @@ function selectBackground(key: BackgroundKey) {
|
|||||||
background-color: #e9edef;
|
background-color: #e9edef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-thumb--custom-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #dfe5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-thumb--custom-empty {
|
||||||
|
background: #252f30;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .bg-thumb--none {
|
.dark .bg-thumb--none {
|
||||||
background-color: #151c1d;
|
background-color: #151c1d;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user