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; }
|
html, body { height: 100%; margin: 0; }
|
||||||
body {
|
body {
|
||||||
background: #fff; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
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-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<link rel="icon" href="icon/icon.png">
|
<link rel="icon" href="icon/icon.png">
|
||||||
<link rel="apple-touch-icon" href="icon/icon.png">
|
<link rel="apple-touch-icon" href="icon/icon.png">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|||||||
@@ -94,10 +94,13 @@ function getLogDirectoryPath(): string {
|
|||||||
return logDir;
|
return logDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
closeAction?: 'quit' | 'minimize' | 'tray';
|
closeAction?: 'quit' | 'minimize' | 'tray';
|
||||||
autoLaunch?: boolean;
|
autoLaunch?: boolean;
|
||||||
launchMinimized?: boolean;
|
launchMinimized?: boolean;
|
||||||
|
lastUsername?: string;
|
||||||
|
splashImageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigPath(): string {
|
function getConfigPath(): string {
|
||||||
@@ -210,7 +213,7 @@ function startSpringBoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startSpringBoot();
|
// startSpringBoot();
|
||||||
function stopSpringBoot() {
|
function stopSpringBoot() {
|
||||||
if (!springProcess) return;
|
if (!springProcess) return;
|
||||||
try {
|
try {
|
||||||
@@ -372,13 +375,26 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
const splashPath = getSplashPath();
|
const splashPath = getSplashPath();
|
||||||
if (existsSync(splashPath)) {
|
if (existsSync(splashPath)) {
|
||||||
|
const config = loadConfig();
|
||||||
|
const imageUrl = config.splashImageUrl || '';
|
||||||
|
console.log('[开屏图片] 启动配置:', { username: config.lastUsername, imageUrl, configPath: getConfigPath() });
|
||||||
|
|
||||||
splashWindow.loadFile(splashPath);
|
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
|
//666
|
||||||
// setTimeout(() => {
|
setTimeout(() => {
|
||||||
// openAppIfNotOpened();
|
openAppIfNotOpened();
|
||||||
// }, 100);
|
}, 100);
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
@@ -769,6 +785,22 @@ ipcMain.handle('window-is-maximized', () => {
|
|||||||
return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false;
|
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> {
|
async function getFileSize(url: string): Promise<number> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ const electronAPI = {
|
|||||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
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) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
ipcRenderer.removeAllListeners('download-progress')
|
ipcRenderer.removeAllListeners('download-progress')
|
||||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
||||||
const RUOYI_BASE = 'http://8.138.23.49:8085';
|
//const RUOYI_BASE = 'http://8.138.23.49:8085';
|
||||||
// const RUOYI_BASE = 'http://192.168.1.89:8085';
|
const RUOYI_BASE = 'http://192.168.1.89:8085';
|
||||||
export const CONFIG = {
|
export const CONFIG = {
|
||||||
CLIENT_BASE: 'http://localhost:8081',
|
CLIENT_BASE: 'http://localhost:8081',
|
||||||
RUOYI_BASE,
|
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)
|
clearInterval(progressTimer)
|
||||||
return taskResult
|
return taskResult
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { User } from '@element-plus/icons-vue'
|
import { User } from '@element-plus/icons-vue'
|
||||||
import { authApi } from '../../api/auth'
|
import { authApi } from '../../api/auth'
|
||||||
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||||
|
import { splashApi } from '../../api/splash'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -41,6 +42,9 @@ async function handleAuth() {
|
|||||||
clientId: deviceId
|
clientId: deviceId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 保存开屏图片配置(不阻塞登录)
|
||||||
|
saveSplashConfigInBackground(authForm.value.username)
|
||||||
|
|
||||||
emit('loginSuccess', {
|
emit('loginSuccess', {
|
||||||
token: loginRes.data.accessToken || loginRes.data.token,
|
token: loginRes.data.accessToken || loginRes.data.token,
|
||||||
permissions: loginRes.data.permissions,
|
permissions: loginRes.data.permissions,
|
||||||
@@ -75,6 +79,17 @@ function resetForm() {
|
|||||||
function showRegister() {
|
function showRegister() {
|
||||||
emit('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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { feedbackApi } from '../../api/feedback'
|
|||||||
import { getToken, getUsernameFromToken } from '../../utils/token'
|
import { getToken, getUsernameFromToken } from '../../utils/token'
|
||||||
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||||
import { updateApi } from '../../api/update'
|
import { updateApi } from '../../api/update'
|
||||||
|
import { splashApi } from '../../api/splash'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -68,6 +69,12 @@ const checkingUpdate = ref(false)
|
|||||||
const hasUpdate = ref(false)
|
const hasUpdate = ref(false)
|
||||||
const autoUpdate = 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({
|
const show = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
@@ -78,6 +85,7 @@ watch(() => props.modelValue, (newVal) => {
|
|||||||
if (newVal) {
|
if (newVal) {
|
||||||
loadAllSettings()
|
loadAllSettings()
|
||||||
loadCurrentVersion()
|
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(() => {
|
onMounted(() => {
|
||||||
loadAllSettings()
|
loadAllSettings()
|
||||||
loadLogDates()
|
loadLogDates()
|
||||||
loadCurrentVersion()
|
loadCurrentVersion()
|
||||||
|
loadSplashImage()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -401,6 +480,12 @@ onMounted(() => {
|
|||||||
<span class="sidebar-icon">🚀</span>
|
<span class="sidebar-icon">🚀</span>
|
||||||
<span class="sidebar-text">启动</span>
|
<span class="sidebar-text">启动</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
:class="['sidebar-item', { active: activeTab === 'splash' }]"
|
||||||
|
@click="scrollToSection('splash')">
|
||||||
|
<span class="sidebar-icon">🖼️</span>
|
||||||
|
<span class="sidebar-text">开屏图片</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="['sidebar-item', { active: activeTab === 'feedback' }]"
|
:class="['sidebar-item', { active: activeTab === 'feedback' }]"
|
||||||
@click="scrollToSection('feedback')">
|
@click="scrollToSection('feedback')">
|
||||||
@@ -537,6 +622,53 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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 id="section-feedback" class="setting-section" @mouseenter="activeTab = 'feedback'">
|
||||||
<div class="section-title">反馈</div>
|
<div class="section-title">反馈</div>
|
||||||
@@ -1179,6 +1311,51 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1F2937;
|
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>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
|||||||
try {
|
try {
|
||||||
logger.info("刷新 Token");
|
logger.info("刷新 Token");
|
||||||
ResponseEntity<?> response = apiForwarder.post("/tool/mark/refreshToken", null, null);
|
ResponseEntity<?> response = apiForwarder.post("/tool/mark/refreshToken", null, null);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.ruoyi.web.controller.monitor;
|
package com.ruoyi.web.controller.monitor;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.ruoyi.common.annotation.Anonymous;
|
import com.ruoyi.common.annotation.Anonymous;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -33,6 +30,17 @@ import com.ruoyi.system.mapper.ClientDeviceMapper;
|
|||||||
import com.ruoyi.system.domain.ClientDevice;
|
import com.ruoyi.system.domain.ClientDevice;
|
||||||
import com.ruoyi.system.mapper.ClientAccountDeviceMapper;
|
import com.ruoyi.system.mapper.ClientAccountDeviceMapper;
|
||||||
import com.ruoyi.system.domain.ClientAccountDevice;
|
import com.ruoyi.system.domain.ClientAccountDevice;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
|
import com.ruoyi.common.config.Qiniu;
|
||||||
|
import com.qiniu.storage.UploadManager;
|
||||||
|
import com.qiniu.util.Auth;
|
||||||
|
import com.qiniu.http.Response;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,11 +55,9 @@ public class ClientAccountController extends BaseController {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IClientAccountService clientAccountService;
|
private IClientAccountService clientAccountService;
|
||||||
|
|
||||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||||
// 访问令牌(3天)
|
// 访问令牌(3天)
|
||||||
private final long JWT_EXPIRATION = 3L * 24 * 60 * 60 * 1000;
|
private final long JWT_EXPIRATION = 3L * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private JwtRsaKeyService jwtRsaKeyService;
|
private JwtRsaKeyService jwtRsaKeyService;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -60,6 +66,16 @@ public class ClientAccountController extends BaseController {
|
|||||||
private ClientDeviceMapper clientDeviceMapper;
|
private ClientDeviceMapper clientDeviceMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ClientAccountDeviceMapper accountDeviceMapper;
|
private ClientAccountDeviceMapper accountDeviceMapper;
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
@Autowired
|
||||||
|
private Qiniu qiniu;
|
||||||
|
@Autowired
|
||||||
|
private UploadManager uploadManager;
|
||||||
|
@Autowired
|
||||||
|
private Auth auth;
|
||||||
|
|
||||||
|
private static final String SPLASH_IMAGE_CACHE_KEY = "splash_image:";
|
||||||
|
|
||||||
private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) {
|
private AjaxResult checkDeviceLimit(Long accountId, String deviceId, int deviceLimit) {
|
||||||
int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId);
|
int activeDeviceCount = accountDeviceMapper.countActiveDevicesByAccountId(accountId);
|
||||||
@@ -159,6 +175,11 @@ public class ClientAccountController extends BaseController {
|
|||||||
// 检查设备限制
|
// 检查设备限制
|
||||||
AjaxResult limitCheck = checkDeviceLimit(account.getId(), clientId, account.getDeviceLimit());
|
AjaxResult limitCheck = checkDeviceLimit(account.getId(), clientId, account.getDeviceLimit());
|
||||||
if (limitCheck != null) return limitCheck;
|
if (limitCheck != null) return limitCheck;
|
||||||
|
|
||||||
|
// 更新开屏图片缓存到 Redis
|
||||||
|
if (StringUtils.isNotEmpty(account.getSplashImage())) {
|
||||||
|
redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, account.getSplashImage());
|
||||||
|
}
|
||||||
|
|
||||||
String token = Jwts.builder()
|
String token = Jwts.builder()
|
||||||
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
|
||||||
@@ -191,7 +212,6 @@ public class ClientAccountController extends BaseController {
|
|||||||
.getBody();
|
.getBody();
|
||||||
String username = (String) claims.get("sub");
|
String username = (String) claims.get("sub");
|
||||||
String clientId = (String) claims.get("clientId");
|
String clientId = (String) claims.get("clientId");
|
||||||
|
|
||||||
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
if (account == null || !"0".equals(account.getStatus())) {
|
if (account == null || !"0".equals(account.getStatus())) {
|
||||||
return AjaxResult.error("token无效");
|
return AjaxResult.error("token无效");
|
||||||
@@ -331,4 +351,66 @@ public class ClientAccountController extends BaseController {
|
|||||||
return AjaxResult.success(Map.of("expireTime", newExpireTime));
|
return AjaxResult.success(Map.of("expireTime", newExpireTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传开屏图片
|
||||||
|
*/
|
||||||
|
@PostMapping("/splash-image/upload")
|
||||||
|
public AjaxResult uploadSplashImage(@RequestParam("file") MultipartFile file, @RequestParam("username") String username) {
|
||||||
|
try {
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account == null) return AjaxResult.error("账号不存在");
|
||||||
|
if (!file.getContentType().startsWith("image/")) return AjaxResult.error("只支持图片文件");
|
||||||
|
if (file.getSize() > 5 * 1024 * 1024) return AjaxResult.error("图片大小不能超过5MB");
|
||||||
|
|
||||||
|
String fileName = "splash/" + DateUtil.format(new Date(), "yyyy/MM/") + IdUtil.simpleUUID() + "." + FileUtil.extName(file.getOriginalFilename());
|
||||||
|
try (InputStream is = file.getInputStream()) {
|
||||||
|
Response res = uploadManager.put(is, fileName, auth.uploadToken(qiniu.getBucket()), null, "");
|
||||||
|
if (!res.isOK()) return AjaxResult.error("上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = qiniu.getResourcesUrl() + fileName;
|
||||||
|
account.setSplashImage(url);
|
||||||
|
clientAccountService.updateClientAccount(account);
|
||||||
|
redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, url);
|
||||||
|
return AjaxResult.success().put("url", url).put("fileName", fileName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return AjaxResult.error("上传失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取开屏图片
|
||||||
|
*/
|
||||||
|
@GetMapping({"/splash-image", "/splash-image/by-username"})
|
||||||
|
public AjaxResult getSplashImage(@RequestParam("username") String username) {
|
||||||
|
String url = redisCache.getCacheObject(SPLASH_IMAGE_CACHE_KEY + username);
|
||||||
|
if (StringUtils.isEmpty(url)) {
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account != null && StringUtils.isNotEmpty(account.getSplashImage())) {
|
||||||
|
url = account.getSplashImage();
|
||||||
|
redisCache.setCacheObject(SPLASH_IMAGE_CACHE_KEY + username, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AjaxResult.success(Map.of("splashImage", url != null ? url : "", "url", url != null ? url : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除开屏图片
|
||||||
|
*/
|
||||||
|
@PostMapping("/splash-image/delete")
|
||||||
|
public AjaxResult deleteSplashImage(@RequestParam("username") String username) {
|
||||||
|
try {
|
||||||
|
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
|
||||||
|
if (account == null) return AjaxResult.error("账号不存在");
|
||||||
|
|
||||||
|
account.setSplashImage(null);
|
||||||
|
clientAccountService.updateClientAccount(account);
|
||||||
|
redisCache.deleteObject(SPLASH_IMAGE_CACHE_KEY + username);
|
||||||
|
return AjaxResult.success("开屏图片已删除");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return AjaxResult.error("删除失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
package com.ruoyi.system.domain;
|
package com.ruoyi.system.domain;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.ruoyi.common.core.domain.BaseEntity;
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
@@ -9,7 +8,6 @@ import com.ruoyi.common.core.domain.BaseEntity;
|
|||||||
*/
|
*/
|
||||||
public class BanmaAccount extends BaseEntity {
|
public class BanmaAccount extends BaseEntity {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
/** 显示名 */
|
/** 显示名 */
|
||||||
private String name;
|
private String name;
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ public class ClientAccount extends BaseEntity
|
|||||||
@Excel(name = "账号类型")
|
@Excel(name = "账号类型")
|
||||||
private String accountType; // trial试用, paid付费
|
private String accountType; // trial试用, paid付费
|
||||||
|
|
||||||
|
/** 开屏图片URL */
|
||||||
|
private String splashImage;
|
||||||
|
|
||||||
public void setId(Long id)
|
public void setId(Long id)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -161,4 +164,14 @@ public class ClientAccount extends BaseEntity
|
|||||||
{
|
{
|
||||||
return accountType;
|
return accountType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSplashImage(String splashImage)
|
||||||
|
{
|
||||||
|
this.splashImage = splashImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSplashImage()
|
||||||
|
{
|
||||||
|
return splashImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="permissions" column="permissions" />
|
<result property="permissions" column="permissions" />
|
||||||
<result property="deviceLimit" column="device_limit" />
|
<result property="deviceLimit" column="device_limit" />
|
||||||
<result property="accountType" column="account_type" />
|
<result property="accountType" column="account_type" />
|
||||||
|
<result property="splashImage" column="splash_image" />
|
||||||
<result property="createBy" column="create_by" />
|
<result property="createBy" column="create_by" />
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
@@ -24,7 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
|
|
||||||
<sql id="selectClientAccountVo">
|
<sql id="selectClientAccountVo">
|
||||||
select id, account_name, username, password, status, expire_time,
|
select id, account_name, username, password, status, expire_time,
|
||||||
allowed_ip_range, remark, permissions, device_limit, account_type, create_by, create_time, update_by, update_time
|
allowed_ip_range, remark, permissions, device_limit, account_type, splash_image, create_by, create_time, update_by, update_time
|
||||||
from client_account
|
from client_account
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="permissions != null">permissions,</if>
|
<if test="permissions != null">permissions,</if>
|
||||||
<if test="deviceLimit != null">device_limit,</if>
|
<if test="deviceLimit != null">device_limit,</if>
|
||||||
<if test="accountType != null">account_type,</if>
|
<if test="accountType != null">account_type,</if>
|
||||||
|
<if test="splashImage != null">splash_image,</if>
|
||||||
<if test="createBy != null">create_by,</if>
|
<if test="createBy != null">create_by,</if>
|
||||||
create_time
|
create_time
|
||||||
</trim>
|
</trim>
|
||||||
@@ -75,6 +77,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="permissions != null">#{permissions},</if>
|
<if test="permissions != null">#{permissions},</if>
|
||||||
<if test="deviceLimit != null">#{deviceLimit},</if>
|
<if test="deviceLimit != null">#{deviceLimit},</if>
|
||||||
<if test="accountType != null">#{accountType},</if>
|
<if test="accountType != null">#{accountType},</if>
|
||||||
|
<if test="splashImage != null">#{splashImage},</if>
|
||||||
<if test="createBy != null">#{createBy},</if>
|
<if test="createBy != null">#{createBy},</if>
|
||||||
sysdate()
|
sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
@@ -93,6 +96,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="permissions != null">permissions = #{permissions},</if>
|
<if test="permissions != null">permissions = #{permissions},</if>
|
||||||
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
<if test="deviceLimit != null">device_limit = #{deviceLimit},</if>
|
||||||
<if test="accountType != null">account_type = #{accountType},</if>
|
<if test="accountType != null">account_type = #{accountType},</if>
|
||||||
|
<if test="splashImage != null">splash_image = #{splashImage},</if>
|
||||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
update_time = sysdate()
|
update_time = sysdate()
|
||||||
</trim>
|
</trim>
|
||||||
|
|||||||
Reference in New Issue
Block a user