完成项目
This commit is contained in:
parent
a6af28e4c5
commit
ba9d40844b
|
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
release
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.vite
|
||||||
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# 使用淘宝镜像源
|
||||||
|
registry=https://registry.npmmirror.com
|
||||||
|
# Electron 镜像
|
||||||
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
# Electron Builder 缓存镜像
|
||||||
|
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||||
|
# Node.js 二进制文件镜像
|
||||||
|
disturl=https://npmmirror.com/mirrors/node/
|
||||||
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Electron 打包说明
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd webapp
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发模式
|
||||||
|
|
||||||
|
### 1. 启动前端开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动 Electron(新终端窗口)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run electron:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 打包为 Windows EXE
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run electron:build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
打包后的文件会在 `release` 目录下。
|
||||||
|
|
||||||
|
## 配置后端 API 地址
|
||||||
|
|
||||||
|
1. 在应用右上角点击 ⚙️ 设置按钮
|
||||||
|
2. 输入后端服务器的完整地址(例如:`http://192.168.1.100:7001`)
|
||||||
|
3. 点击"保存设置"
|
||||||
|
4. 刷新页面以应用新设置
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- Electron 应用使用 `electron-store` 存储配置,配置文件保存在用户数据目录
|
||||||
|
- Web 版本使用 `localStorage` 存储配置
|
||||||
|
- 设置页面支持测试连接功能,可以验证后端服务器是否可访问
|
||||||
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 动态导入 electron-store(ES 模块)
|
||||||
|
let store;
|
||||||
|
let storePromise = import('electron-store').then((StoreModule) => {
|
||||||
|
const Store = StoreModule.default;
|
||||||
|
store = new Store();
|
||||||
|
return store;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.cjs'),
|
||||||
|
webSecurity: false, // 允许跨域请求(用于访问后端 API)
|
||||||
|
},
|
||||||
|
// icon: path.join(__dirname, '../assets/icon.png'), // 可选:应用图标(需要提供图标文件)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载应用
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:加载 Vite 开发服务器
|
||||||
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
} else {
|
||||||
|
// 生产环境:加载打包后的文件
|
||||||
|
const indexPath = path.join(__dirname, '../dist/index.html');
|
||||||
|
mainWindow.loadFile(indexPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
// 确保 store 已初始化
|
||||||
|
await storePromise;
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC 处理:获取/设置 API 地址
|
||||||
|
ipcMain.handle('get-api-url', async () => {
|
||||||
|
await storePromise;
|
||||||
|
return store.get('apiUrl', 'http://192.168.1.148:7001');
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('set-api-url', async (event, url) => {
|
||||||
|
await storePromise;
|
||||||
|
store.set('apiUrl', url);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-ws-url', async () => {
|
||||||
|
await storePromise;
|
||||||
|
const apiUrl = store.get('apiUrl', 'http://192.168.1.148:7001');
|
||||||
|
// 将 http:// 转换为 ws://, https:// 转换为 wss://
|
||||||
|
return apiUrl.replace(/^http/, 'ws');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// 暴露受保护的方法给渲染进程
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
getApiUrl: () => ipcRenderer.invoke('get-api-url'),
|
||||||
|
setApiUrl: (url) => ipcRenderer.invoke('set-api-url', url),
|
||||||
|
getWsUrl: () => ipcRenderer.invoke('get-ws-url'),
|
||||||
|
});
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -2,18 +2,60 @@
|
||||||
"name": "web-karaoke-station",
|
"name": "web-karaoke-station",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"description": "Web点歌台 - 在线音乐点播系统",
|
||||||
|
"author": "Web点歌台",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "electron/main.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"electron:dev": "cross-env NODE_ENV=development ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron electron/main.cjs",
|
||||||
|
"electron:build": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run build && electron-builder",
|
||||||
|
"electron:build:win": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run build && electron-builder --win"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0",
|
||||||
|
"electron-store": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0",
|
||||||
|
"electron": "^28.0.0",
|
||||||
|
"electron-builder": "^24.9.1",
|
||||||
|
"cross-env": "^7.0.3"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.webkaraoke.station",
|
||||||
|
"productName": "Web点歌台",
|
||||||
|
"electronDownload": {
|
||||||
|
"mirror": "https://npmmirror.com/mirrors/electron/"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/**/*.cjs",
|
||||||
|
"node_modules/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icon": "assets/icon.ico"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,12 @@
|
||||||
<button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
|
<button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
|
||||||
{{ isFullscreen ? '🗗' : '🗖' }}
|
{{ isFullscreen ? '🗗' : '🗖' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="refresh-btn" @click="handleRefresh" title="刷新页面">
|
<button class="refresh-btn" @click="handleRefresh" title="刷新页面">
|
||||||
🔄
|
🔄
|
||||||
</button>
|
</button>
|
||||||
|
<button class="settings-btn" @click="handleSettings" title="设置">
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="app-main">
|
<main class="app-main">
|
||||||
|
|
@ -125,8 +128,10 @@ onMounted(async () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载播放状态失败:', error)
|
console.error('加载播放状态失败:', error)
|
||||||
}
|
}
|
||||||
// 初始化WebSocket连接
|
// 初始化WebSocket连接(使用 async/await)
|
||||||
initWebSocket(onQueueUpdate, onPlaybackStateUpdate, onOnlineCountUpdate, onAnnouncement, onVolumeControl, onUserId)
|
initWebSocket(onQueueUpdate, onPlaybackStateUpdate, onOnlineCountUpdate, onAnnouncement, onVolumeControl, onUserId).catch(err => {
|
||||||
|
console.error('WebSocket初始化失败:', err)
|
||||||
|
})
|
||||||
|
|
||||||
// 监听全屏状态变化
|
// 监听全屏状态变化
|
||||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
|
@ -205,6 +210,18 @@ function handleRefresh() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开设置页面
|
||||||
|
function handleSettings() {
|
||||||
|
// 在 Electron 中使用 hash 路由,在浏览器中使用正常路由
|
||||||
|
if (window.electronAPI) {
|
||||||
|
// Electron 环境:使用 hash 路由
|
||||||
|
window.location.hash = '#/settings'
|
||||||
|
} else {
|
||||||
|
// 浏览器环境:直接跳转
|
||||||
|
window.location.href = '/settings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 全屏/退出全屏
|
// 全屏/退出全屏
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
|
|
@ -286,15 +303,30 @@ function handleFullscreenChange() {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen-btn:hover,
|
.fullscreen-btn:hover,
|
||||||
.refresh-btn:hover {
|
.refresh-btn:hover,
|
||||||
background: #f1f5f9;
|
.settings-btn:hover {
|
||||||
}
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
.fullscreen-btn:active,
|
.fullscreen-btn:active,
|
||||||
.refresh-btn:active {
|
.refresh-btn:active,
|
||||||
background: #e2e8f0;
|
.settings-btn:active {
|
||||||
}
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
// 管理员API
|
// 管理员API
|
||||||
const ADMIN_BASE_URL = ''
|
import { getBaseUrl } from '../utils/config'
|
||||||
|
|
||||||
|
let adminBaseUrlCache = ''
|
||||||
|
|
||||||
|
// 获取基础URL(带缓存)
|
||||||
|
async function getCachedBaseUrl() {
|
||||||
|
if (!adminBaseUrlCache) {
|
||||||
|
adminBaseUrlCache = await getBaseUrl()
|
||||||
|
}
|
||||||
|
return adminBaseUrlCache
|
||||||
|
}
|
||||||
|
|
||||||
function getAdminToken() {
|
function getAdminToken() {
|
||||||
return localStorage.getItem('admin_token') || ''
|
return localStorage.getItem('admin_token') || ''
|
||||||
|
|
@ -7,8 +17,10 @@ function getAdminToken() {
|
||||||
|
|
||||||
async function request(url, options = {}) {
|
async function request(url, options = {}) {
|
||||||
const token = getAdminToken()
|
const token = getAdminToken()
|
||||||
|
const baseUrl = await getCachedBaseUrl()
|
||||||
|
const fullUrl = baseUrl ? `${baseUrl}${url}` : url
|
||||||
|
|
||||||
const response = await fetch(`${ADMIN_BASE_URL}${url}`, {
|
const response = await fetch(fullUrl, {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -123,3 +135,10 @@ export async function adminSeek(time) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除 API 地址缓存(当API地址改变时调用)
|
||||||
|
*/
|
||||||
|
export function clearAdminBaseUrlCache() {
|
||||||
|
adminBaseUrlCache = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,27 @@
|
||||||
// Midway后端API
|
// Midway后端API
|
||||||
// 使用相对路径,通过Vite代理转发到后端
|
import { getBaseUrl } from '../utils/config'
|
||||||
const BASE_URL = ''
|
|
||||||
|
let baseUrlCache = ''
|
||||||
|
|
||||||
|
// 获取基础URL(带缓存)
|
||||||
|
async function getCachedBaseUrl() {
|
||||||
|
if (!baseUrlCache) {
|
||||||
|
baseUrlCache = await getBaseUrl()
|
||||||
|
}
|
||||||
|
return baseUrlCache
|
||||||
|
}
|
||||||
|
|
||||||
async function request(path, options = {}) {
|
async function request(path, options = {}) {
|
||||||
// 使用相对路径,Vite会通过代理转发到后端
|
// 获取基础URL
|
||||||
|
const baseUrl = await getCachedBaseUrl()
|
||||||
|
|
||||||
|
// 构建完整URL
|
||||||
const url = path.startsWith('/') ? path : `/${path}`
|
const url = path.startsWith('/') ? path : `/${path}`
|
||||||
|
const fullUrl = baseUrl ? `${baseUrl}${url}` : url
|
||||||
const { method = 'GET', body, params = {} } = options
|
const { method = 'GET', body, params = {} } = options
|
||||||
|
|
||||||
// 构建URL,添加查询参数
|
// 构建URL,添加查询参数
|
||||||
let requestUrl = url
|
let requestUrl = fullUrl
|
||||||
if (method === 'GET' && params && Object.keys(params).length > 0) {
|
if (method === 'GET' && params && Object.keys(params).length > 0) {
|
||||||
const searchParams = new URLSearchParams()
|
const searchParams = new URLSearchParams()
|
||||||
Object.entries(params).forEach(([k, v]) => {
|
Object.entries(params).forEach(([k, v]) => {
|
||||||
|
|
@ -180,8 +193,8 @@ export async function getPlaybackState() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBaseUrl() {
|
// 清除缓存(当API地址改变时调用)
|
||||||
// 返回空字符串,使用相对路径通过代理
|
export function clearBaseUrlCache() {
|
||||||
return ''
|
baseUrlCache = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
// WebSocket连接管理
|
// WebSocket连接管理
|
||||||
// 使用相对路径,通过Vite代理转发到后端
|
import { getWsUrl } from '../utils/config'
|
||||||
// 在开发环境下,Vite会代理WebSocket连接
|
|
||||||
const WS_BASE_URL = import.meta.env.DEV
|
let wsBaseUrlCache = ''
|
||||||
? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`
|
|
||||||
: 'ws://localhost:7001'
|
// 获取 WebSocket 基础URL(带缓存)
|
||||||
|
async function getCachedWsBaseUrl() {
|
||||||
|
if (!wsBaseUrlCache) {
|
||||||
|
wsBaseUrlCache = await getWsUrl()
|
||||||
|
}
|
||||||
|
return wsBaseUrlCache
|
||||||
|
}
|
||||||
|
|
||||||
let ws = null
|
let ws = null
|
||||||
let reconnectTimer = null
|
let reconnectTimer = null
|
||||||
|
|
@ -27,15 +33,18 @@ function getClientId() {
|
||||||
/**
|
/**
|
||||||
* 初始化WebSocket连接
|
* 初始化WebSocket连接
|
||||||
*/
|
*/
|
||||||
export function initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onAnnouncement, onVolumeControl, onUserId) {
|
export async function initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onAnnouncement, onVolumeControl, onUserId) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
console.log('WebSocket已经连接')
|
console.log('WebSocket已经连接')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取 WebSocket 基础URL
|
||||||
|
const wsBaseUrl = await getCachedWsBaseUrl()
|
||||||
|
|
||||||
// 获取客户端唯一标识
|
// 获取客户端唯一标识
|
||||||
const clientId = getClientId()
|
const clientId = getClientId()
|
||||||
const wsUrl = `${WS_BASE_URL}/ws?clientId=${encodeURIComponent(clientId)}`
|
const wsUrl = `${wsBaseUrl}/ws?clientId=${encodeURIComponent(clientId)}`
|
||||||
console.log('正在连接WebSocket:', wsUrl)
|
console.log('正在连接WebSocket:', wsUrl)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -89,8 +98,8 @@ export function initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onA
|
||||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||||
reconnectAttempts++
|
reconnectAttempts++
|
||||||
console.log(`尝试重连 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
|
console.log(`尝试重连 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
|
||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(async () => {
|
||||||
initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onAnnouncement, onVolumeControl, onUserId)
|
await initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onAnnouncement, onVolumeControl, onUserId)
|
||||||
}, RECONNECT_DELAY)
|
}, RECONNECT_DELAY)
|
||||||
} else {
|
} else {
|
||||||
console.error('WebSocket重连失败,已达到最大重试次数')
|
console.error('WebSocket重连失败,已达到最大重试次数')
|
||||||
|
|
@ -137,3 +146,10 @@ export function getWebSocketStatus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除 WebSocket URL 缓存(当API地址改变时调用)
|
||||||
|
*/
|
||||||
|
export function clearWsUrlCache() {
|
||||||
|
wsBaseUrlCache = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
import { defineProps, ref, watch, nextTick, toRefs, onMounted, onBeforeUnmount, computed } from 'vue'
|
import { defineProps, ref, watch, nextTick, toRefs, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||||
import { getLyric } from '../api/backend'
|
import { getLyric } from '../api/backend'
|
||||||
import { parseLrc } from '../utils/lrc'
|
import { parseLrc } from '../utils/lrc'
|
||||||
|
import { getBaseUrl } from '../utils/config'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectedSong: { type: Object, default: null },
|
selectedSong: { type: Object, default: null },
|
||||||
|
|
@ -88,9 +89,43 @@ const currentSong = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// 流URL(后端代理)
|
// 流URL(后端代理)
|
||||||
const streamUrl = computed(() => {
|
const streamUrl = ref(null)
|
||||||
if (!currentSong.value?.id) return null
|
|
||||||
return `/api/music/stream/${currentSong.value.id}?level=standard`
|
// 更新流URL
|
||||||
|
async function updateStreamUrl() {
|
||||||
|
if (!currentSong.value?.id) {
|
||||||
|
streamUrl.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Electron 环境中,需要使用完整的 API URL
|
||||||
|
if (window.electronAPI || window.location.protocol === 'file:') {
|
||||||
|
try {
|
||||||
|
const baseUrl = await getBaseUrl()
|
||||||
|
streamUrl.value = `${baseUrl}/api/music/stream/${currentSong.value.id}?level=standard`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取流URL失败:', error)
|
||||||
|
streamUrl.value = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 浏览器环境使用相对路径(通过 Vite 代理)
|
||||||
|
streamUrl.value = `/api/music/stream/${currentSong.value.id}?level=standard`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听当前歌曲变化,更新流URL
|
||||||
|
watch(() => currentSong.value?.id, () => {
|
||||||
|
updateStreamUrl()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听 streamUrl 变化,确保音频元素 src 更新
|
||||||
|
watch(streamUrl, (newUrl) => {
|
||||||
|
if (audioRef.value && newUrl) {
|
||||||
|
console.log('更新音频源URL:', newUrl)
|
||||||
|
// 强制重新加载音频
|
||||||
|
audioRef.value.src = newUrl
|
||||||
|
audioRef.value.load()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatDuration(sec) {
|
function formatDuration(sec) {
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,81 @@ import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import AdminLogin from './pages/AdminLogin.vue'
|
import AdminLogin from './pages/AdminLogin.vue'
|
||||||
import AdminPanel from './pages/AdminPanel.vue'
|
import AdminPanel from './pages/AdminPanel.vue'
|
||||||
|
import Settings from './pages/Settings.vue'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
// 根据路径决定加载哪个组件
|
// 获取当前路径(支持 Electron 的 file:// 协议)
|
||||||
const path = window.location.pathname
|
function getCurrentPath() {
|
||||||
|
// 在 Electron 的 file:// 协议下,pathname 可能总是返回 '/'
|
||||||
let app
|
// 优先使用 hash 路由(Electron 环境)
|
||||||
|
if (window.electronAPI) {
|
||||||
if (path.startsWith('/admin')) {
|
const hash = window.location.hash
|
||||||
if (path === '/admin/login') {
|
if (hash && hash.startsWith('#/')) {
|
||||||
// 管理员登录页面
|
return hash.substring(1) // 去掉 # 号,返回如 '/settings'
|
||||||
app = createApp(AdminLogin)
|
|
||||||
} else if (path === '/admin') {
|
|
||||||
// 管理员控制面板
|
|
||||||
// 检查token
|
|
||||||
const token = localStorage.getItem('admin_token')
|
|
||||||
if (!token) {
|
|
||||||
window.location.href = '/admin/login'
|
|
||||||
app = createApp(AdminLogin)
|
|
||||||
} else {
|
|
||||||
app = createApp(AdminPanel)
|
|
||||||
}
|
}
|
||||||
} else {
|
return '/'
|
||||||
// 其他admin路径,默认跳转到登录页
|
|
||||||
window.location.href = '/admin/login'
|
|
||||||
app = createApp(AdminLogin)
|
|
||||||
}
|
}
|
||||||
} else {
|
// 浏览器环境:使用 pathname
|
||||||
// 主应用
|
return window.location.pathname
|
||||||
app = createApp(App)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mount('#app')
|
// 根据路径决定加载哪个组件
|
||||||
|
function loadApp() {
|
||||||
|
const path = getCurrentPath()
|
||||||
|
|
||||||
|
// 如果已经有 app 实例,先卸载
|
||||||
|
if (window.currentApp) {
|
||||||
|
window.currentApp.unmount()
|
||||||
|
}
|
||||||
|
|
||||||
|
let app
|
||||||
|
|
||||||
|
if (path.startsWith('/admin')) {
|
||||||
|
if (path === '/admin/login') {
|
||||||
|
// 管理员登录页面
|
||||||
|
app = createApp(AdminLogin)
|
||||||
|
} else if (path === '/admin') {
|
||||||
|
// 管理员控制面板
|
||||||
|
// 检查token
|
||||||
|
const token = localStorage.getItem('admin_token')
|
||||||
|
if (!token) {
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.history.pushState({}, '', '/admin/login')
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
|
} else {
|
||||||
|
window.location.href = '/admin/login'
|
||||||
|
}
|
||||||
|
app = createApp(AdminLogin)
|
||||||
|
} else {
|
||||||
|
app = createApp(AdminPanel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他admin路径,默认跳转到登录页
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.history.pushState({}, '', '/admin/login')
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||||
|
} else {
|
||||||
|
window.location.href = '/admin/login'
|
||||||
|
}
|
||||||
|
app = createApp(AdminLogin)
|
||||||
|
}
|
||||||
|
} else if (path === '/settings') {
|
||||||
|
// 设置页面
|
||||||
|
app = createApp(Settings)
|
||||||
|
} else {
|
||||||
|
// 主应用
|
||||||
|
app = createApp(App)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
window.currentApp = app
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化(支持 Electron)
|
||||||
|
window.addEventListener('popstate', loadApp)
|
||||||
|
window.addEventListener('hashchange', loadApp)
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
loadApp()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -371,7 +371,9 @@ onMounted(() => {
|
||||||
loadUsers()
|
loadUsers()
|
||||||
|
|
||||||
// 初始化WebSocket(只监听播放状态)
|
// 初始化WebSocket(只监听播放状态)
|
||||||
initWebSocket(() => {}, onPlaybackStateUpdate, () => {}, () => {}, () => {}, () => {})
|
initWebSocket(() => {}, onPlaybackStateUpdate, () => {}, () => {}, () => {}, () => {}).catch(err => {
|
||||||
|
console.error('WebSocket初始化失败:', err)
|
||||||
|
})
|
||||||
|
|
||||||
// 定期刷新播放状态和用户列表
|
// 定期刷新播放状态和用户列表
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
<template>
|
||||||
|
<div class="settings-page">
|
||||||
|
<header class="settings-header">
|
||||||
|
<h1>设置</h1>
|
||||||
|
<button class="btn-back" @click="handleBack">返回</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="settings-main">
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>后端服务器配置</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API 地址</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="apiUrl"
|
||||||
|
placeholder="http://localhost:7001"
|
||||||
|
@input="validateUrl"
|
||||||
|
/>
|
||||||
|
<p class="help-text">请输入后端服务器的完整地址(包含 http:// 或 https://)</p>
|
||||||
|
<p v-if="urlError" class="error-text">{{ urlError }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>WebSocket 地址</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="wsUrl"
|
||||||
|
placeholder="ws://localhost:7001"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<p class="help-text">WebSocket 地址会根据 API 地址自动生成</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn-save" @click="handleSave" :disabled="loading || !!urlError">
|
||||||
|
{{ loading ? '保存中...' : '保存设置' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-test" @click="handleTest" :disabled="loading || !!urlError">
|
||||||
|
测试连接
|
||||||
|
</button>
|
||||||
|
<button class="btn-reset" @click="handleReset">
|
||||||
|
重置为默认值
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="testResult" class="test-result" :class="{ success: testResult.success, error: !testResult.success }">
|
||||||
|
<p>{{ testResult.message }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
import { setApiUrl, getApiUrl, getWsUrl } from '../utils/config'
|
||||||
|
import { clearBaseUrlCache } from '../api/backend'
|
||||||
|
import { closeWebSocket, clearWsUrlCache } from '../api/websocket'
|
||||||
|
import { clearAdminBaseUrlCache } from '../api/admin'
|
||||||
|
|
||||||
|
const apiUrl = ref('http://localhost:7001')
|
||||||
|
const wsUrl = ref('ws://localhost:7001')
|
||||||
|
const urlError = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const testResult = ref(null)
|
||||||
|
|
||||||
|
// 加载当前配置
|
||||||
|
async function loadSettings() {
|
||||||
|
try {
|
||||||
|
const savedApiUrl = await getApiUrl()
|
||||||
|
apiUrl.value = savedApiUrl
|
||||||
|
updateWsUrl()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载设置失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 WebSocket URL
|
||||||
|
function updateWsUrl() {
|
||||||
|
wsUrl.value = apiUrl.value.replace(/^http/, 'ws')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 URL 格式
|
||||||
|
function validateUrl() {
|
||||||
|
urlError.value = ''
|
||||||
|
testResult.value = null
|
||||||
|
|
||||||
|
if (!apiUrl.value) {
|
||||||
|
urlError.value = 'API 地址不能为空'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(apiUrl.value)
|
||||||
|
// 验证协议
|
||||||
|
if (!apiUrl.value.startsWith('http://') && !apiUrl.value.startsWith('https://')) {
|
||||||
|
urlError.value = 'URL 必须以 http:// 或 https:// 开头'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
urlError.value = '无效的 URL 格式'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWsUrl()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 API URL 变化
|
||||||
|
watch(apiUrl, () => {
|
||||||
|
validateUrl()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
async function handleSave() {
|
||||||
|
if (!validateUrl()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await setApiUrl(apiUrl.value)
|
||||||
|
// 清除所有缓存
|
||||||
|
clearBaseUrlCache()
|
||||||
|
clearWsUrlCache()
|
||||||
|
clearAdminBaseUrlCache()
|
||||||
|
// 关闭现有连接,让应用重新连接
|
||||||
|
closeWebSocket()
|
||||||
|
|
||||||
|
testResult.value = {
|
||||||
|
success: true,
|
||||||
|
message: '设置已保存!请刷新页面以应用新设置。'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提示刷新
|
||||||
|
setTimeout(() => {
|
||||||
|
if (confirm('设置已保存,是否立即刷新页面?')) {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
} catch (error) {
|
||||||
|
testResult.value = {
|
||||||
|
success: false,
|
||||||
|
message: '保存失败: ' + error.message
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
async function handleTest() {
|
||||||
|
if (!validateUrl()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
testResult.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 测试 API 连接
|
||||||
|
const testUrl = `${apiUrl.value}/api/music/queue`
|
||||||
|
const response = await fetch(testUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
testResult.value = {
|
||||||
|
success: true,
|
||||||
|
message: '连接成功!服务器可以正常访问。'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testResult.value = {
|
||||||
|
success: false,
|
||||||
|
message: `连接失败: HTTP ${response.status}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
testResult.value = {
|
||||||
|
success: false,
|
||||||
|
message: '连接失败: ' + (error.message || '无法连接到服务器')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置为默认值
|
||||||
|
function handleReset() {
|
||||||
|
apiUrl.value = 'http://192.168.1.148:7001'
|
||||||
|
updateWsUrl()
|
||||||
|
urlError.value = ''
|
||||||
|
testResult.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
function handleBack() {
|
||||||
|
// 在 Electron 中使用 hash 路由返回
|
||||||
|
if (window.electronAPI) {
|
||||||
|
window.location.hash = ''
|
||||||
|
} else {
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #64748b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-main {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h2 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[readonly] {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save,
|
||||||
|
.btn-test,
|
||||||
|
.btn-reset {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:disabled {
|
||||||
|
background: #93c5fd;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test:hover:not(:disabled) {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test:disabled {
|
||||||
|
background: #6ee7b7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset:hover {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result.success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
border: 1px solid #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
// 配置管理工具
|
||||||
|
// 支持从 localStorage 或 Electron 获取 API 地址
|
||||||
|
|
||||||
|
const DEFAULT_API_URL = 'http://localhost:7001';
|
||||||
|
const DEFAULT_WS_URL = 'ws://localhost:7001';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 API 地址
|
||||||
|
*/
|
||||||
|
export async function getApiUrl() {
|
||||||
|
// 检查是否在 Electron 环境中
|
||||||
|
if (window.electronAPI) {
|
||||||
|
try {
|
||||||
|
return await window.electronAPI.getApiUrl();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 Electron API 地址失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 localStorage 获取
|
||||||
|
const saved = localStorage.getItem('api_url');
|
||||||
|
if (saved) {
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境使用默认值
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
return DEFAULT_API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境(Electron 或 Web):尝试从当前域名推断,否则使用默认值
|
||||||
|
// 在 Electron 中,如果使用 file:// 协议,则使用默认值
|
||||||
|
if (window.location.protocol === 'file:') {
|
||||||
|
return DEFAULT_API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
|
||||||
|
const host = window.location.host;
|
||||||
|
return `${protocol}//${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 API 地址
|
||||||
|
*/
|
||||||
|
export async function setApiUrl(url) {
|
||||||
|
// 验证 URL 格式
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('无效的 URL 格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否在 Electron 环境中
|
||||||
|
if (window.electronAPI) {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.setApiUrl(url);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置 Electron API 地址失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 localStorage
|
||||||
|
localStorage.setItem('api_url', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 WebSocket 地址
|
||||||
|
*/
|
||||||
|
export async function getWsUrl() {
|
||||||
|
// 检查是否在 Electron 环境中
|
||||||
|
if (window.electronAPI) {
|
||||||
|
try {
|
||||||
|
return await window.electronAPI.getWsUrl();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 Electron WebSocket 地址失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 localStorage 获取 API 地址并转换为 WebSocket 地址
|
||||||
|
const apiUrl = await getApiUrl();
|
||||||
|
return apiUrl.replace(/^http/, 'ws');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基础 URL(用于 API 请求)
|
||||||
|
*/
|
||||||
|
export async function getBaseUrl() {
|
||||||
|
const apiUrl = await getApiUrl();
|
||||||
|
|
||||||
|
// 如果在 Electron 或生产环境中,直接使用 API 地址
|
||||||
|
if (window.electronAPI || !import.meta.env.DEV) {
|
||||||
|
return apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境:使用相对路径,通过 Vite 代理
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
base: './', // 使用相对路径,便于 Electron 打包
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
host: true,
|
host: true,
|
||||||
|
|
@ -21,8 +22,15 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 确保所有路由都返回 index.html(SPA 路由支持)
|
build: {
|
||||||
// Vite 开发服务器默认支持 history API fallback
|
outDir: 'dist',
|
||||||
|
// 确保所有路由都返回 index.html(SPA 路由支持)
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: './index.html',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2088
webapp/yarn.lock
2088
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
|
|
@ -29,7 +29,7 @@ export class AdminController {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminToken = process.env.ADMIN_TOKEN || 'admin123';
|
const adminToken = process.env.ADMIN_TOKEN || 'admin1a3sd21as5d3asd21w3a524sd1w3a52s1da23';
|
||||||
|
|
||||||
if (token !== adminToken) {
|
if (token !== adminToken) {
|
||||||
this.ctx.status = 403;
|
this.ctx.status = 403;
|
||||||
|
|
@ -48,7 +48,7 @@ export class AdminController {
|
||||||
*/
|
*/
|
||||||
@Post('/login')
|
@Post('/login')
|
||||||
async login(@Body() body: { password: string }) {
|
async login(@Body() body: { password: string }) {
|
||||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin';
|
const adminPassword = process.env.ADMIN_PASSWORD || 'adminYeeywwaea1sd23w1a32sd1was53dw';
|
||||||
|
|
||||||
if (body.password !== adminPassword) {
|
if (body.password !== adminPassword) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -59,7 +59,7 @@ export class AdminController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回token(实际生产环境应使用JWT等更安全的方式)
|
// 返回token(实际生产环境应使用JWT等更安全的方式)
|
||||||
const token = process.env.ADMIN_TOKEN || 'admin123';
|
const token = process.env.ADMIN_TOKEN || 'admin1a3sd21as5d3asd21w3a524sd1w3a52s1da23';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export class AdminMiddleware implements IMiddleware<Context, NextFunction> {
|
||||||
|
|
||||||
// 简单的token验证(实际生产环境应使用更安全的方式)
|
// 简单的token验证(实际生产环境应使用更安全的方式)
|
||||||
// 这里使用配置中的admin token
|
// 这里使用配置中的admin token
|
||||||
const adminToken = process.env.ADMIN_TOKEN || 'admin123';
|
const adminToken = process.env.ADMIN_TOKEN || 'admin1a3sd21as5d3asd21w3a524sd1w3a52s1da23';
|
||||||
|
|
||||||
if (token !== adminToken) {
|
if (token !== adminToken) {
|
||||||
ctx.status = 403;
|
ctx.status = 403;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue