feat(client): 实现自定义开屏图片功能

- 在 ClientAccount 实体中新增 splashImage 字段用于存储开屏图片URL
- 在 ClientAccountController 中添加上传、获取和删除开屏图片的接口
- 集成七牛云存储实现图片上传功能,支持图片格式和大小校验
- 使用 Redis 缓存开屏图片URL,提升访问性能
- 在客户端登录成功后异步加载并保存开屏图片配置
- 新增 splashApi 模块封装开屏图片相关HTTP请求- 在主进程中实现开屏图片配置的持久化存储和读取
- 在设置页面中增加开屏图片管理界面,支持上传、预览和删除操作
- 修改 splash.html 支持动态加载自定义开屏图片
- 调整 CSP 策略允许加载本地和HTTPS图片资源
This commit is contained in:
2025-11-08 10:23:45 +08:00
parent 7c7009ffed
commit c2e1617a99
13 changed files with 374 additions and 19 deletions

View File

@@ -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<number> {
return new Promise((resolve) => {

View File

@@ -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))

View File

@@ -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,

View File

@@ -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}`)
}
}

View File

@@ -359,7 +359,6 @@ async function startTrademarkQuery() {
// 其他错误(网络错误等)继续等待
}
}
clearInterval(progressTimer)
return taskResult
}

View File

@@ -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)
}
}
</script>
<template>

View File

@@ -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<HTMLInputElement | null>(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()
})
</script>
@@ -401,6 +480,12 @@ onMounted(() => {
<span class="sidebar-icon">🚀</span>
<span class="sidebar-text">启动</span>
</div>
<div
:class="['sidebar-item', { active: activeTab === 'splash' }]"
@click="scrollToSection('splash')">
<span class="sidebar-icon">🖼</span>
<span class="sidebar-text">开屏图片</span>
</div>
<div
:class="['sidebar-item', { active: activeTab === 'feedback' }]"
@click="scrollToSection('feedback')">
@@ -537,6 +622,53 @@ onMounted(() => {
</div>
</div>
<!-- 开屏图片设置 -->
<div id="section-splash" class="setting-section" @mouseenter="activeTab = 'splash'">
<div class="section-title">开屏图片</div>
<div class="section-subtitle-text">自定义应用启动时的开屏图片</div>
<div class="setting-item">
<div class="splash-preview-container" v-if="splashImageUrl">
<img :src="splashImageUrl" alt="开屏图片预览" class="splash-preview-image" />
</div>
<div class="splash-placeholder" v-else>
<span class="placeholder-icon">🖼</span>
<span class="placeholder-text">未设置自定义开屏图片</span>
</div>
</div>
<div class="setting-item" style="margin-top: 10px;">
<div class="splash-actions">
<input
ref="fileInputRef"
type="file"
accept="image/*"
@change="handleSplashImageUpload"
style="display: none;"
/>
<el-button
type="primary"
size="small"
:loading="uploadingSplashImage"
:disabled="uploadingSplashImage"
@click="triggerFileSelect">
{{ uploadingSplashImage ? '上传中...' : '选择图片' }}
</el-button>
<el-button
v-if="splashImageUrl"
size="small"
:loading="deletingSplashImage"
:disabled="deletingSplashImage"
@click="handleDeleteSplashImage">
{{ deletingSplashImage ? '删除中...' : '删除' }}
</el-button>
</div>
<div class="setting-desc" style="margin-top: 6px;">
支持 JPGPNG 格式大小不超过 5MB建议尺寸 1200x675
</div>
</div>
</div>
<!-- 反馈页面 -->
<div id="section-feedback" class="setting-section" @mouseenter="activeTab = 'feedback'">
<div class="section-title">反馈</div>
@@ -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;
}
</style>
<script lang="ts">