From c2e1617a99a64b208cbc19284c5c5ca42be6b4d3 Mon Sep 17 00:00:00 2001
From: zhangzijienbplus <17738440858@163.com>
Date: Sat, 8 Nov 2025 10:23:45 +0800
Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=AE=9E=E7=8E=B0=E8=87=AA?=
=?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=BC=80=E5=B1=8F=E5=9B=BE=E7=89=87=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在 ClientAccount 实体中新增 splashImage 字段用于存储开屏图片URL
- 在 ClientAccountController 中添加上传、获取和删除开屏图片的接口
- 集成七牛云存储实现图片上传功能,支持图片格式和大小校验
- 使用 Redis 缓存开屏图片URL,提升访问性能
- 在客户端登录成功后异步加载并保存开屏图片配置
- 新增 splashApi 模块封装开屏图片相关HTTP请求- 在主进程中实现开屏图片配置的持久化存储和读取
- 在设置页面中增加开屏图片管理界面,支持上传、预览和删除操作
- 修改 splash.html 支持动态加载自定义开屏图片
- 调整 CSP 策略允许加载本地和HTTPS图片资源
---
electron-vue-template/public/splash.html | 4 +-
electron-vue-template/src/main/main.ts | 40 +++-
electron-vue-template/src/main/preload.ts | 4 +
.../src/renderer/api/http.ts | 4 +-
.../src/renderer/api/splash.ts | 32 ++++
.../components/amazon/TrademarkCheckPanel.vue | 1 -
.../renderer/components/auth/LoginDialog.vue | 15 ++
.../components/common/SettingsDialog.vue | 177 ++++++++++++++++++
.../service/impl/FangzhouApiServiceImpl.java | 1 -
.../monitor/ClientAccountController.java | 94 +++++++++-
.../com/ruoyi/system/domain/BanmaAccount.java | 2 -
.../ruoyi/system/domain/ClientAccount.java | 13 ++
.../mapper/system/ClientAccountMapper.xml | 6 +-
13 files changed, 374 insertions(+), 19 deletions(-)
create mode 100644 electron-vue-template/src/renderer/api/splash.ts
diff --git a/electron-vue-template/public/splash.html b/electron-vue-template/public/splash.html
index 92c6270..ff5fd65 100644
--- a/electron-vue-template/public/splash.html
+++ b/electron-vue-template/public/splash.html
@@ -8,7 +8,7 @@
html, body { height: 100%; margin: 0; }
body {
background: #fff; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
- background-image: url('./image/splash_screen.png');
+ background-image: var(--splash-image, url('./image/splash_screen.png'));
background-repeat: no-repeat;
background-position: center;
background-size: cover;
@@ -21,7 +21,7 @@
-
+
diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts
index 6bc4c7f..9aca48f 100644
--- a/electron-vue-template/src/main/main.ts
+++ b/electron-vue-template/src/main/main.ts
@@ -94,10 +94,13 @@ function getLogDirectoryPath(): string {
return logDir;
}
+
interface AppConfig {
closeAction?: 'quit' | 'minimize' | 'tray';
autoLaunch?: boolean;
launchMinimized?: boolean;
+ lastUsername?: string;
+ splashImageUrl?: string;
}
function getConfigPath(): string {
@@ -210,7 +213,7 @@ function startSpringBoot() {
}
}
- startSpringBoot();
+ // startSpringBoot();
function stopSpringBoot() {
if (!springProcess) return;
try {
@@ -372,13 +375,26 @@ app.whenReady().then(() => {
const splashPath = getSplashPath();
if (existsSync(splashPath)) {
+ const config = loadConfig();
+ const imageUrl = config.splashImageUrl || '';
+ console.log('[开屏图片] 启动配置:', { username: config.lastUsername, imageUrl, configPath: getConfigPath() });
+
splashWindow.loadFile(splashPath);
+
+ if (imageUrl) {
+ splashWindow.webContents.once('did-finish-load', () => {
+ splashWindow?.webContents.executeJavaScript(`
+ document.body.style.setProperty('--splash-image', "url('${imageUrl}')");
+ `).then(() => console.log('[开屏图片] 注入成功:', imageUrl))
+ .catch(err => console.error('[开屏图片] 注入失败:', err));
+ });
+ }
}
}
//666
-// setTimeout(() => {
-// openAppIfNotOpened();
-// }, 100);
+ setTimeout(() => {
+ openAppIfNotOpened();
+ }, 100);
app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -769,6 +785,22 @@ ipcMain.handle('window-is-maximized', () => {
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
});
+// 保存开屏图片配置(用户名 + URL)
+ipcMain.handle('save-splash-config', (event, username: string, imageUrl: string) => {
+ const config = loadConfig();
+ config.lastUsername = username;
+ config.splashImageUrl = imageUrl;
+ saveConfig(config);
+ console.log('[开屏图片] 已保存配置:', { username, imageUrl, path: getConfigPath() });
+ return { success: true };
+});
+
+// 获取开屏图片配置
+ipcMain.handle('get-splash-config', () => {
+ const config = loadConfig();
+ return { username: config.lastUsername || '', imageUrl: config.splashImageUrl || '' };
+});
+
async function getFileSize(url: string): Promise {
return new Promise((resolve) => {
diff --git a/electron-vue-template/src/main/preload.ts b/electron-vue-template/src/main/preload.ts
index 0bd3117..a54291b 100644
--- a/electron-vue-template/src/main/preload.ts
+++ b/electron-vue-template/src/main/preload.ts
@@ -43,6 +43,10 @@ const electronAPI = {
windowClose: () => ipcRenderer.invoke('window-close'),
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
+ // 开屏图片相关 API
+ saveSplashConfig: (username: string, imageUrl: string) => ipcRenderer.invoke('save-splash-config', username, imageUrl),
+ getSplashConfig: () => ipcRenderer.invoke('get-splash-config'),
+
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.removeAllListeners('download-progress')
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
diff --git a/electron-vue-template/src/renderer/api/http.ts b/electron-vue-template/src/renderer/api/http.ts
index 249dbf8..a6481b3 100644
--- a/electron-vue-template/src/renderer/api/http.ts
+++ b/electron-vue-template/src/renderer/api/http.ts
@@ -1,6 +1,6 @@
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
-const RUOYI_BASE = 'http://8.138.23.49:8085';
-// const RUOYI_BASE = 'http://192.168.1.89:8085';
+//const RUOYI_BASE = 'http://8.138.23.49:8085';
+ const RUOYI_BASE = 'http://192.168.1.89:8085';
export const CONFIG = {
CLIENT_BASE: 'http://localhost:8081',
RUOYI_BASE,
diff --git a/electron-vue-template/src/renderer/api/splash.ts b/electron-vue-template/src/renderer/api/splash.ts
new file mode 100644
index 0000000..13ff308
--- /dev/null
+++ b/electron-vue-template/src/renderer/api/splash.ts
@@ -0,0 +1,32 @@
+import { http } from './http'
+
+export interface SplashImageResponse {
+ splashImage: string
+ url: string
+}
+
+export const splashApi = {
+ // 上传开屏图片
+ async uploadSplashImage(file: File, username: string) {
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('username', username)
+ return http.upload<{ data: { url: string; fileName: string } }>('/monitor/account/splash-image/upload', formData)
+ },
+
+ // 获取当前用户的开屏图片
+ async getSplashImage(username: string) {
+ return http.get<{ data: SplashImageResponse }>('/monitor/account/splash-image', { username })
+ },
+
+ // 根据用户名获取开屏图片(用于启动时)
+ async getSplashImageByUsername(username: string) {
+ return http.get<{ data: SplashImageResponse }>('/monitor/account/splash-image/by-username', { username })
+ },
+
+ // 删除自定义开屏图片(恢复默认)
+ async deleteSplashImage(username: string) {
+ return http.post<{ data: string }>(`/monitor/account/splash-image/delete?username=${username}`)
+ }
+}
+
diff --git a/electron-vue-template/src/renderer/components/amazon/TrademarkCheckPanel.vue b/electron-vue-template/src/renderer/components/amazon/TrademarkCheckPanel.vue
index 2efe0f9..0c999de 100644
--- a/electron-vue-template/src/renderer/components/amazon/TrademarkCheckPanel.vue
+++ b/electron-vue-template/src/renderer/components/amazon/TrademarkCheckPanel.vue
@@ -359,7 +359,6 @@ async function startTrademarkQuery() {
// 其他错误(网络错误等)继续等待
}
}
-
clearInterval(progressTimer)
return taskResult
}
diff --git a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue
index 65fb81c..02b9a33 100644
--- a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue
+++ b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue
@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
import { User } from '@element-plus/icons-vue'
import { authApi } from '../../api/auth'
import { getOrCreateDeviceId } from '../../utils/deviceId'
+import { splashApi } from '../../api/splash'
interface Props {
modelValue: boolean
@@ -41,6 +42,9 @@ async function handleAuth() {
clientId: deviceId
})
+ // 保存开屏图片配置(不阻塞登录)
+ saveSplashConfigInBackground(authForm.value.username)
+
emit('loginSuccess', {
token: loginRes.data.accessToken || loginRes.data.token,
permissions: loginRes.data.permissions,
@@ -75,6 +79,17 @@ function resetForm() {
function showRegister() {
emit('showRegister')
}
+
+// 保存开屏图片配置
+async function saveSplashConfigInBackground(username: string) {
+ try {
+ const res = await splashApi.getSplashImage(username)
+ const url = res?.data?.data?.url || res?.data?.url || ''
+ await (window as any).electronAPI.saveSplashConfig(username, url)
+ } catch (error) {
+ console.error('[开屏图片] 保存配置失败:', error)
+ }
+}
diff --git a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue
index e7fc503..31094f8 100644
--- a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue
+++ b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue
@@ -12,6 +12,7 @@ import { feedbackApi } from '../../api/feedback'
import { getToken, getUsernameFromToken } from '../../utils/token'
import { getOrCreateDeviceId } from '../../utils/deviceId'
import { updateApi } from '../../api/update'
+import { splashApi } from '../../api/splash'
interface Props {
modelValue: boolean
@@ -68,6 +69,12 @@ const checkingUpdate = ref(false)
const hasUpdate = ref(false)
const autoUpdate = ref(false)
+// 开屏图片相关
+const splashImageUrl = ref('')
+const uploadingSplashImage = ref(false)
+const deletingSplashImage = ref(false)
+const fileInputRef = ref(null)
+
const show = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
@@ -78,6 +85,7 @@ watch(() => props.modelValue, (newVal) => {
if (newVal) {
loadAllSettings()
loadCurrentVersion()
+ loadSplashImage()
}
})
@@ -361,10 +369,81 @@ async function submitFeedback() {
}
}
+// 触发文件选择
+function triggerFileSelect() {
+ fileInputRef.value?.click()
+}
+
+// 加载开屏图片
+async function loadSplashImage() {
+ try {
+ const username = getUsernameFromToken()
+ if (!username) return
+
+ const res = await splashApi.getSplashImage(username)
+ splashImageUrl.value = res?.data?.data?.url || res?.data?.url || ''
+ } catch (error) {
+ splashImageUrl.value = ''
+ }
+}
+
+// 上传开屏图片
+async function handleSplashImageUpload(event: Event) {
+ const input = event.target as HTMLInputElement
+ if (!input.files || input.files.length === 0) return
+
+ const file = input.files[0]
+ const username = getUsernameFromToken()
+ if (!username) return ElMessage.warning('请先登录')
+ if (!file.type.startsWith('image/')) return ElMessage.error('只支持图片文件')
+ if (file.size > 5 * 1024 * 1024) return ElMessage.error('图片大小不能超过5MB')
+
+ try {
+ uploadingSplashImage.value = true
+ const res = await splashApi.uploadSplashImage(file, username)
+ const url = res?.data?.data?.url || res?.data?.url
+ if (url) {
+ splashImageUrl.value = url
+ await (window as any).electronAPI.saveSplashConfig(username, url)
+ ElMessage.success('开屏图片设置成功,重启应用后生效')
+ }
+ } catch (error: any) {
+ ElMessage.error(error?.message || '上传失败')
+ } finally {
+ uploadingSplashImage.value = false
+ input.value = ''
+ }
+}
+
+// 删除开屏图片
+async function handleDeleteSplashImage() {
+ try {
+ await ElMessageBox.confirm(
+ '确定要删除自定义开屏图片吗?将恢复为默认开屏图片。',
+ '确认删除',
+ { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
+ )
+
+ deletingSplashImage.value = true
+ const username = getUsernameFromToken()
+ if (!username) return ElMessage.warning('请先登录')
+
+ await splashApi.deleteSplashImage(username)
+ await (window as any).electronAPI.saveSplashConfig(username, '')
+ splashImageUrl.value = ''
+ ElMessage.success('开屏图片已删除,重启应用后恢复默认')
+ } catch (error: any) {
+ if (error !== 'cancel') ElMessage.error(error?.message || '删除失败')
+ } finally {
+ deletingSplashImage.value = false
+ }
+}
+
onMounted(() => {
loadAllSettings()
loadLogDates()
loadCurrentVersion()
+ loadSplashImage()
})
@@ -401,6 +480,12 @@ onMounted(() => {
+
+
+
+
@@ -537,6 +622,53 @@ onMounted(() => {
+
+
+
开屏图片
+
自定义应用启动时的开屏图片
+
+
+
+
![开屏图片预览]()
+
+
+ 🖼️
+ 未设置自定义开屏图片
+
+
+
+
+
+
+
+ {{ uploadingSplashImage ? '上传中...' : '选择图片' }}
+
+
+ {{ deletingSplashImage ? '删除中...' : '删除' }}
+
+
+
+ 支持 JPG、PNG 格式,大小不超过 5MB,建议尺寸 1200x675
+
+
+
+
反馈
@@ -1179,6 +1311,51 @@ onMounted(() => {
font-weight: 600;
color: #1F2937;
}
+
+/* 开屏图片样式 */
+.splash-preview-container {
+ width: 100%;
+ height: 180px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid #E5E6EB;
+ background: #F8F9FA;
+}
+
+.splash-preview-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.splash-placeholder {
+ width: 100%;
+ height: 180px;
+ border-radius: 6px;
+ border: 2px dashed #E5E6EB;
+ background: #F8F9FA;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+.placeholder-icon {
+ font-size: 32px;
+ opacity: 0.5;
+}
+
+.placeholder-text {
+ font-size: 13px;
+ color: #86909C;
+}
+
+.splash-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}