feat(client): 实现自定义开屏图片功能
- 在 ClientAccount 实体中新增 splashImage 字段用于存储开屏图片URL - 在 ClientAccountController 中添加上传、获取和删除开屏图片的接口 - 集成七牛云存储实现图片上传功能,支持图片格式和大小校验 - 使用 Redis 缓存开屏图片URL,提升访问性能 - 在客户端登录成功后异步加载并保存开屏图片配置 - 新增 splashApi 模块封装开屏图片相关HTTP请求- 在主进程中实现开屏图片配置的持久化存储和读取 - 在设置页面中增加开屏图片管理界面,支持上传、预览和删除操作 - 修改 splash.html 支持动态加载自定义开屏图片 - 调整 CSP 策略允许加载本地和HTTPS图片资源
This commit is contained in:
@@ -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 @@
|
||||
<link rel="icon" href="icon/icon.png">
|
||||
<link rel="apple-touch-icon" href="icon/icon.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data: file: https:; style-src 'self' 'unsafe-inline';">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
32
electron-vue-template/src/renderer/api/splash.ts
Normal file
32
electron-vue-template/src/renderer/api/splash.ts
Normal 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}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +359,6 @@ async function startTrademarkQuery() {
|
||||
// 其他错误(网络错误等)继续等待
|
||||
}
|
||||
}
|
||||
|
||||
clearInterval(progressTimer)
|
||||
return taskResult
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;">
|
||||
支持 JPG、PNG 格式,大小不超过 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">
|
||||
|
||||
Reference in New Issue
Block a user