electron应用改造
This commit is contained in:
parent
e990d6c67a
commit
3460944825
6
.env.electron
Normal file
6
.env.electron
Normal file
@ -0,0 +1,6 @@
|
||||
# Electron renderer build
|
||||
VITE_OPEN = false
|
||||
VITE_OPEN_CDN = false
|
||||
VITE_PUBLIC_PATH = ./
|
||||
VITE_API_URL = /api
|
||||
VITE_IS_ELECTRON = true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
/release
|
||||
/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/
|
||||
|
||||
|
||||
|
||||
20780
.swagger-default.json
20780
.swagger-default.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
45
ELECTRON.md
Normal file
45
ELECTRON.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Electron 桌面端
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
pnpm electron:dev
|
||||
```
|
||||
|
||||
该命令会同时启动 Vite 开发服务器和 Electron,支持前端热更新。
|
||||
|
||||
## Windows 打包
|
||||
|
||||
```bash
|
||||
pnpm electron:build
|
||||
```
|
||||
|
||||
安装包输出到 `release/`。只生成免安装目录用于调试时,可运行:
|
||||
|
||||
```bash
|
||||
pnpm electron:build:dir
|
||||
```
|
||||
|
||||
## API 地址
|
||||
|
||||
打包后的应用右上角提供“接口环境”切换器,支持:
|
||||
|
||||
- 测试服:`http://8.153.108.90:9996`
|
||||
- 正式服:`https://event.yuedongsports.com`
|
||||
|
||||
选择结果保存在本机,下次启动会继续使用上一次的环境。切换环境时应用会清除当前登录状态,防止跨环境复用 Token。
|
||||
Electron 的登录 Token 使用窗口级 `sessionStorage` 保存,以兼容 `ydevent://` 自定义协议;关闭应用后需要重新登录。
|
||||
|
||||
启动应用前也可以通过环境变量覆盖地址:
|
||||
|
||||
```powershell
|
||||
$env:ELECTRON_TEST_API_URL = 'http://localhost:9996'
|
||||
$env:ELECTRON_API_ENV = 'test'
|
||||
pnpm electron:dev
|
||||
```
|
||||
|
||||
可用的覆盖变量为 `ELECTRON_TEST_API_URL` 和 `ELECTRON_PRODUCTION_API_URL`。
|
||||
|
||||
## 成绩投屏
|
||||
|
||||
Electron 中打开成绩或检录投屏时,会创建独立的无边框全屏窗口。存在多块显示器时,窗口自动放到主操作窗口以外的另一块屏幕;只有一块显示器时,则在当前屏幕全屏显示。
|
||||
216
electron/main.cjs
Normal file
216
electron/main.cjs
Normal file
@ -0,0 +1,216 @@
|
||||
const { app, BrowserWindow, ipcMain, net, protocol, screen, shell } = require('electron');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
|
||||
const APP_SCHEME = 'ydevent';
|
||||
const API_ENVIRONMENTS = {
|
||||
test: {
|
||||
label: '测试服',
|
||||
origin: process.env.ELECTRON_TEST_API_URL || 'http://8.153.108.90:9996',
|
||||
},
|
||||
production: {
|
||||
label: '正式服',
|
||||
origin: process.env.ELECTRON_PRODUCTION_API_URL || process.env.ELECTRON_API_URL || 'https://event.yuedongsports.com',
|
||||
},
|
||||
};
|
||||
|
||||
let selectedApiEnvironment = process.env.ELECTRON_API_ENV === 'test' ? 'test' : 'production';
|
||||
let mainWindow = null;
|
||||
let presentationWindow = null;
|
||||
|
||||
const environmentConfigPath = () => path.join(app.getPath('userData'), 'environment.json');
|
||||
|
||||
const loadApiEnvironment = () => {
|
||||
try {
|
||||
const saved = JSON.parse(fs.readFileSync(environmentConfigPath(), 'utf8'));
|
||||
if (API_ENVIRONMENTS[saved.environment]) selectedApiEnvironment = saved.environment;
|
||||
} catch {
|
||||
// 首次运行时使用默认环境。
|
||||
}
|
||||
};
|
||||
|
||||
const saveApiEnvironment = (environment) => {
|
||||
fs.mkdirSync(path.dirname(environmentConfigPath()), { recursive: true });
|
||||
fs.writeFileSync(environmentConfigPath(), JSON.stringify({ environment }, null, 2), 'utf8');
|
||||
};
|
||||
|
||||
const apiEnvironmentState = () => ({
|
||||
current: selectedApiEnvironment,
|
||||
environments: Object.entries(API_ENVIRONMENTS).map(([value, item]) => ({ value, label: item.label, origin: item.origin })),
|
||||
});
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: APP_SCHEME,
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const registerAppProtocol = () => {
|
||||
const rendererDirectory = path.resolve(__dirname, '..', 'dist');
|
||||
|
||||
protocol.handle(APP_SCHEME, async (request) => {
|
||||
const requestUrl = new URL(request.url);
|
||||
|
||||
if (requestUrl.pathname === '/api' || requestUrl.pathname.startsWith('/api/')) {
|
||||
const apiOrigin = API_ENVIRONMENTS[selectedApiEnvironment].origin;
|
||||
const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, apiOrigin);
|
||||
const options = {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
};
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
options.body = request.body;
|
||||
options.duplex = 'half';
|
||||
}
|
||||
|
||||
return net.fetch(targetUrl.toString(), options);
|
||||
}
|
||||
|
||||
const pathname = decodeURIComponent(requestUrl.pathname);
|
||||
const requestedPath = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
||||
let filePath = path.resolve(rendererDirectory, requestedPath);
|
||||
|
||||
if (!filePath.startsWith(`${rendererDirectory}${path.sep}`) && filePath !== rendererDirectory) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (!path.extname(filePath)) {
|
||||
filePath = path.join(rendererDirectory, 'index.html');
|
||||
}
|
||||
|
||||
return net.fetch(pathToFileURL(filePath).toString());
|
||||
});
|
||||
};
|
||||
|
||||
const targetPresentationDisplay = () => {
|
||||
const displays = screen.getAllDisplays();
|
||||
if (!mainWindow || displays.length < 2) return displays[0] || screen.getPrimaryDisplay();
|
||||
const operatorDisplay = screen.getDisplayMatching(mainWindow.getBounds());
|
||||
return displays.find((display) => display.id !== operatorDisplay.id) || operatorDisplay;
|
||||
};
|
||||
|
||||
const positionPresentationWindow = () => {
|
||||
if (!presentationWindow || presentationWindow.isDestroyed()) return;
|
||||
const targetDisplay = targetPresentationDisplay();
|
||||
const currentDisplay = screen.getDisplayMatching(presentationWindow.getBounds());
|
||||
if (currentDisplay.id !== targetDisplay.id) {
|
||||
presentationWindow.setFullScreen(false);
|
||||
presentationWindow.setBounds(targetDisplay.bounds);
|
||||
}
|
||||
if (!presentationWindow.isFullScreen()) presentationWindow.setFullScreen(true);
|
||||
};
|
||||
|
||||
const openPresentationWindow = async (rawUrl) => {
|
||||
const baseUrl = mainWindow?.webContents.getURL() || `${APP_SCHEME}://app/index.html`;
|
||||
const targetUrl = new URL(rawUrl, baseUrl);
|
||||
const isAppUrl = targetUrl.protocol === `${APP_SCHEME}:`;
|
||||
const isDevUrl = !app.isPackaged && ['127.0.0.1', 'localhost'].includes(targetUrl.hostname);
|
||||
if (!isAppUrl && !isDevUrl) throw new Error('不允许在投屏窗口中打开外部地址');
|
||||
|
||||
if (!presentationWindow || presentationWindow.isDestroyed()) {
|
||||
const display = targetPresentationDisplay();
|
||||
presentationWindow = new BrowserWindow({
|
||||
x: display.bounds.x,
|
||||
y: display.bounds.y,
|
||||
width: display.bounds.width,
|
||||
height: display.bounds.height,
|
||||
frame: false,
|
||||
fullscreen: true,
|
||||
autoHideMenuBar: true,
|
||||
show: false,
|
||||
backgroundColor: '#020936',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
presentationWindow.on('closed', () => {
|
||||
presentationWindow = null;
|
||||
});
|
||||
presentationWindow.once('ready-to-show', () => presentationWindow?.showInactive());
|
||||
} else {
|
||||
positionPresentationWindow();
|
||||
}
|
||||
|
||||
await presentationWindow.loadURL(targetUrl.toString());
|
||||
presentationWindow.setFullScreen(true);
|
||||
presentationWindow.showInactive();
|
||||
return { opened: true, displayId: targetPresentationDisplay().id };
|
||||
};
|
||||
|
||||
const registerIpcHandlers = () => {
|
||||
ipcMain.handle('app:get-api-environment', () => apiEnvironmentState());
|
||||
ipcMain.handle('app:set-api-environment', (_event, environment) => {
|
||||
if (!API_ENVIRONMENTS[environment]) throw new Error('未知接口环境');
|
||||
selectedApiEnvironment = environment;
|
||||
saveApiEnvironment(environment);
|
||||
if (presentationWindow && !presentationWindow.isDestroyed()) presentationWindow.reload();
|
||||
return apiEnvironmentState();
|
||||
});
|
||||
ipcMain.handle('presentation:open', (_event, url) => openPresentationWindow(url));
|
||||
};
|
||||
|
||||
const createWindow = () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 900,
|
||||
minWidth: 1100,
|
||||
minHeight: 700,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
icon: path.join(__dirname, '..', 'public', 'app.ico'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.once('ready-to-show', () => mainWindow?.show());
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
void shell.openExternal(url);
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
if (app.isPackaged) {
|
||||
void mainWindow.loadURL(`${APP_SCHEME}://app/index.html`);
|
||||
} else {
|
||||
void mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL || 'http://127.0.0.1:9991');
|
||||
mainWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
if (presentationWindow && !presentationWindow.isDestroyed()) presentationWindow.close();
|
||||
mainWindow = null;
|
||||
});
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
loadApiEnvironment();
|
||||
registerAppProtocol();
|
||||
registerIpcHandlers();
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
13
electron/preload.cjs
Normal file
13
electron/preload.cjs
Normal file
@ -0,0 +1,13 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
isDesktop: true,
|
||||
platform: process.platform,
|
||||
apiEnvironment: {
|
||||
get: () => ipcRenderer.invoke('app:get-api-environment'),
|
||||
set: (environment) => ipcRenderer.invoke('app:set-api-environment', environment),
|
||||
},
|
||||
presentation: {
|
||||
open: (url) => ipcRenderer.invoke('presentation:open', url),
|
||||
},
|
||||
});
|
||||
50
package.json
50
package.json
@ -2,12 +2,16 @@
|
||||
"name": "yd-admin",
|
||||
"version": "2.4.33",
|
||||
"description": "vue3 vite next admin template",
|
||||
"main": "electron/main.cjs",
|
||||
"author": "lyt_20201208",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node scripts/build-and-upload-oss.cjs",
|
||||
"build:local": "vite build --base ./",
|
||||
"electron:dev": "concurrently -k \"vite --mode electron\" \"wait-on http://127.0.0.1:9991 && electron .\"",
|
||||
"electron:build": "vite build --mode electron --outDir dist && electron-builder",
|
||||
"electron:build:dir": "vite build --mode electron --outDir dist && electron-builder --dir",
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/",
|
||||
"gen": "sta -p http://localhost:53785/swagger/Default/swagger.json -o ./src/apis --modular --route-types --axios --module-name-index 1 --extract-request-params -t ./templates"
|
||||
},
|
||||
@ -19,6 +23,7 @@
|
||||
"axios": "^1.3.5",
|
||||
"countup.js": "^2.6.0",
|
||||
"cropperjs": "^1.5.13",
|
||||
"dayjs": "^1.11.23",
|
||||
"echarts": "^5.4.2",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
@ -56,6 +61,10 @@
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
"concurrently": "^10.0.5",
|
||||
"electron": "33.4.11",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-builder-squirrel-windows": "24.13.3",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-vue": "^9.10.0",
|
||||
"prettier": "^2.8.7",
|
||||
@ -65,7 +74,8 @@
|
||||
"vite-plugin-cdn-import": "^0.3.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-setup-extend-plus": "^0.1.0",
|
||||
"vue-eslint-parser": "^9.1.1"
|
||||
"vue-eslint-parser": "^9.1.1",
|
||||
"wait-on": "^9.1.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
@ -79,6 +89,12 @@
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">= 7.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"electron-winstaller"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"vue3",
|
||||
@ -91,5 +107,37 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/lyt-top/vue-next-admin.git"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.yuedong.event",
|
||||
"productName": "跃动赛事",
|
||||
"asar": true,
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"electron/**/*",
|
||||
"public/app.ico",
|
||||
"package.json"
|
||||
],
|
||||
"win": {
|
||||
"icon": "public/app.ico",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": "always",
|
||||
"createStartMenuShortcut": true
|
||||
},
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}"
|
||||
}
|
||||
}
|
||||
|
||||
1990
pnpm-lock.yaml
generated
1990
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
75
src/App.vue
75
src/App.vue
@ -6,6 +6,17 @@
|
||||
<CloseFull v-if="!themeConfig.isLockScreen" />
|
||||
<Upgrade v-if="getVersion" />
|
||||
<Sponsors />
|
||||
<div v-if="showApiEnvironmentSwitcher" class="electron-environment-switcher">
|
||||
<span>接口环境</span>
|
||||
<el-select v-model="selectedApiEnvironment" size="small" :loading="apiEnvironmentChanging" @change="changeApiEnvironment">
|
||||
<el-option
|
||||
v-for="item in apiEnvironmentOptions"
|
||||
:key="item.value"
|
||||
:label="`${item.label} · ${item.origin}`"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
@ -20,6 +31,7 @@ import other from '/@/utils/other';
|
||||
import { Local, Session } from '/@/utils/storage';
|
||||
import mittBus from '/@/utils/mitt';
|
||||
import setIntroduction from '/@/utils/setIconfont';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
// 引入组件
|
||||
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
|
||||
@ -35,6 +47,42 @@ const route = useRoute();
|
||||
const stores = useTagsViewRoutes();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const selectedApiEnvironment = ref<'test' | 'production'>('production');
|
||||
const currentApiEnvironment = ref<'test' | 'production'>('production');
|
||||
const apiEnvironmentOptions = ref<ApiEnvironmentState['environments']>([]);
|
||||
const apiEnvironmentChanging = ref(false);
|
||||
const showApiEnvironmentSwitcher = computed(
|
||||
() => Boolean(window.electron?.isDesktop) && route.meta.isPublic !== true && route.query.presentation !== '1'
|
||||
);
|
||||
|
||||
const loadApiEnvironment = async () => {
|
||||
if (!window.electron?.apiEnvironment) return;
|
||||
const state = await window.electron.apiEnvironment.get();
|
||||
selectedApiEnvironment.value = state.current;
|
||||
currentApiEnvironment.value = state.current;
|
||||
apiEnvironmentOptions.value = state.environments;
|
||||
};
|
||||
|
||||
const changeApiEnvironment = async (environment: 'test' | 'production') => {
|
||||
if (!window.electron?.apiEnvironment || environment === currentApiEnvironment.value) return;
|
||||
const target = apiEnvironmentOptions.value.find((item) => item.value === environment);
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`即将切换到${target?.label || environment},当前登录状态会被清除,是否继续?`,
|
||||
'切换接口环境',
|
||||
{ type: 'warning', confirmButtonText: '确认切换', cancelButtonText: '取消' }
|
||||
);
|
||||
apiEnvironmentChanging.value = true;
|
||||
await window.electron.apiEnvironment.set(environment);
|
||||
Session.clear();
|
||||
window.location.reload();
|
||||
} catch (reason) {
|
||||
selectedApiEnvironment.value = currentApiEnvironment.value;
|
||||
if (reason !== 'cancel' && reason !== 'close') ElMessage.error('接口环境切换失败');
|
||||
} finally {
|
||||
apiEnvironmentChanging.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 设置锁屏时组件显示隐藏
|
||||
const setLockScreen = computed(() => {
|
||||
@ -67,7 +115,8 @@ onBeforeMount(() => {
|
||||
setIntroduction.jsCdn();
|
||||
});
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadApiEnvironment();
|
||||
nextTick(() => {
|
||||
// 监听布局配'置弹窗点击打开
|
||||
mittBus.on('openSetingsDrawer', () => {
|
||||
@ -99,3 +148,27 @@ watch(
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.electron-environment-switcher {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
right: 18px;
|
||||
top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 9px;
|
||||
color: var(--el-text-color-regular);
|
||||
background: color-mix(in srgb, var(--el-bg-color) 94%, transparent);
|
||||
box-shadow: 0 7px 24px rgba(20, 45, 80, 0.12);
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.electron-environment-switcher .el-select {
|
||||
width: 240px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import pinia from '/@/stores/index';
|
||||
@ -32,7 +32,8 @@ const { isRequestRoutes } = themeConfig.value;
|
||||
* @link 参考:https://next.router.vuejs.org/zh/api/#createrouter
|
||||
*/
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
// Electron 的自定义协议下使用 Hash 路由,Web 端仍保持 History 路由。
|
||||
history: import.meta.env.VITE_IS_ELECTRON === 'true' ? createWebHashHistory() : createWebHistory(),
|
||||
/**
|
||||
* 说明:
|
||||
* 1、notFoundAndNoPower 默认添加 404、401 界面,防止一直提示 No match found for location with path 'xxx'
|
||||
|
||||
16
src/types/global.d.ts
vendored
16
src/types/global.d.ts
vendored
@ -30,8 +30,24 @@ declare interface Window {
|
||||
nextLoading: boolean;
|
||||
BMAP_SATELLITE_MAP: any;
|
||||
BMap: any;
|
||||
electron?: {
|
||||
isDesktop: boolean;
|
||||
platform: string;
|
||||
apiEnvironment: {
|
||||
get: () => Promise<ApiEnvironmentState>;
|
||||
set: (environment: 'test' | 'production') => Promise<ApiEnvironmentState>;
|
||||
};
|
||||
presentation: {
|
||||
open: (url: string) => Promise<{ opened: boolean; displayId: number }>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
declare type ApiEnvironmentState = {
|
||||
current: 'test' | 'production';
|
||||
environments: Array<{ value: 'test' | 'production'; label: string; origin: string }>;
|
||||
};
|
||||
|
||||
// 声明路由当前项类型
|
||||
declare type RouteItem<T = any> = {
|
||||
path: string;
|
||||
|
||||
@ -42,10 +42,23 @@ export const Local = {
|
||||
export const Session = {
|
||||
// 设置临时缓存
|
||||
set(key: string, val: any) {
|
||||
if (window.electron?.isDesktop) {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(val));
|
||||
return;
|
||||
}
|
||||
return Cookies.set(key, JSON.stringify(val));
|
||||
},
|
||||
// 获取临时缓存
|
||||
get<T = any>(key: string) {
|
||||
if (window.electron?.isDesktop) {
|
||||
const value = window.sessionStorage.getItem(key);
|
||||
if (value === null) return undefined;
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return value as T;
|
||||
}
|
||||
}
|
||||
const v = Cookies.get(key);
|
||||
if (v !== undefined) {
|
||||
return JSON.parse(v) as T;
|
||||
@ -54,10 +67,18 @@ export const Session = {
|
||||
},
|
||||
// 移除临时缓存
|
||||
remove(key: string) {
|
||||
if (window.electron?.isDesktop) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
return;
|
||||
}
|
||||
return Cookies.remove(key);
|
||||
},
|
||||
// 移除全部临时缓存
|
||||
clear() {
|
||||
if (window.electron?.isDesktop) {
|
||||
window.sessionStorage.clear();
|
||||
return;
|
||||
}
|
||||
Object.keys(Cookies.get()).forEach(function (cookieName) {
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
|
||||
@ -225,20 +225,28 @@ const onSignIn = async () => {
|
||||
formRef.value!.validate(async (valid) => {
|
||||
if (valid) {
|
||||
state.loading.signIn = true;
|
||||
const { statusCode } = await login(state.ruleForm);
|
||||
if (statusCode === 200) {
|
||||
if (!themeConfig.value.isRequestRoutes) {
|
||||
const isNoPower = await initFrontEndControlRoutes();
|
||||
signInSuccess(isNoPower);
|
||||
try {
|
||||
const { statusCode } = await login(state.ruleForm);
|
||||
if (statusCode === 200) {
|
||||
if (!themeConfig.value.isRequestRoutes) {
|
||||
const isNoPower = await initFrontEndControlRoutes();
|
||||
signInSuccess(isNoPower);
|
||||
} else {
|
||||
const isNoPower = await initBackEndControlRoutes();
|
||||
signInSuccess(isNoPower);
|
||||
}
|
||||
} else {
|
||||
const isNoPower = await initBackEndControlRoutes();
|
||||
signInSuccess(isNoPower);
|
||||
onCaptchaChange();
|
||||
state.ruleForm.code = '';
|
||||
}
|
||||
} else {
|
||||
} catch {
|
||||
NextLoading.done();
|
||||
onCaptchaChange();
|
||||
state.ruleForm.code = '';
|
||||
ElMessage.error('登录后初始化失败,请检查接口环境或网络后重试');
|
||||
} finally {
|
||||
state.loading.signIn = false;
|
||||
}
|
||||
state.loading.signIn = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="scoringCast">
|
||||
import { computed, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
import ScoreScreen from '/@/views/scoring/screen/index.vue';
|
||||
@ -14,10 +14,16 @@ import CheckinScreen from '/@/views/scoring/checkin-screen/index.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const storesTagsViewRoutes = useTagsViewRoutes();
|
||||
const presentationMode = ref(null);
|
||||
const currentMode = computed(() =>
|
||||
route.query.castMode === 'checkin' || route.query.mode === 'checkin' ? 'checkin' : 'live'
|
||||
presentationMode.value || (route.query.castMode === 'checkin' || route.query.mode === 'checkin' ? 'checkin' : 'live')
|
||||
);
|
||||
|
||||
function receivePresentationMode(event) {
|
||||
if (event.origin !== window.location.origin || event.data?.type !== 'yd-event-presentation-mode') return;
|
||||
if (event.data.mode === 'live' || event.data.mode === 'checkin') presentationMode.value = event.data.mode;
|
||||
}
|
||||
|
||||
function requestPresentationFullscreen() {
|
||||
if (route.query.autofullscreen !== '1' || document.fullscreenElement) return;
|
||||
document.documentElement.requestFullscreen?.({ navigationUI: 'hide' }).catch(() => {});
|
||||
@ -25,10 +31,14 @@ function requestPresentationFullscreen() {
|
||||
|
||||
onMounted(() => {
|
||||
storesTagsViewRoutes.setCurrenFullscreen(true);
|
||||
window.addEventListener('message', receivePresentationMode);
|
||||
requestPresentationFullscreen();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => storesTagsViewRoutes.setCurrenFullscreen(false));
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', receivePresentationMode);
|
||||
storesTagsViewRoutes.setCurrenFullscreen(false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -68,11 +68,12 @@ import { checkinScreenCastGet } from '/@/api/match/workbench';
|
||||
|
||||
const route=useRoute();
|
||||
const storesTagsViewRoutes=useTagsViewRoutes();
|
||||
const fullscreenDocument=route.query.presentation==='1'&&window.parent!==window?window.parent.document:document;
|
||||
const viewportRef=ref(null),scale=ref(1);
|
||||
let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-context')||'{}')}catch{savedContext={}}
|
||||
const eventId=Number(route.query.eventId)||Number(savedContext.eventId)||0;
|
||||
const isBigScreen=computed(()=>storesTagsViewRoutes.isTagsViewCurrenFull);
|
||||
const fullscreenButtonText=computed(()=>document.fullscreenElement?'退出全屏':'全屏展示');
|
||||
const fullscreenButtonText=computed(()=>fullscreenDocument.fullscreenElement?'退出全屏':'全屏展示');
|
||||
const stageStyle=computed(()=>({transform:`translate(-50%, -50%) scale(${scale.value})`}));
|
||||
const data=ref({stages:[],units:[]}),offline=ref(false),clock=ref(new Date()),controlsVisible=ref(false),unitPage=ref(0);
|
||||
let pollTimer,clockTimer,controlTimer,unitTimer,serverBaseMs=Date.now(),syncedAtMs=Date.now(),requesting=false;
|
||||
@ -88,7 +89,7 @@ function resize(){const host=viewportRef.value;if(!host)return;scale.value=Math.
|
||||
function tick(){clock.value=new Date(serverBaseMs+(Date.now()-syncedAtMs))}
|
||||
function syncClock(){const raw=data.value.serverTime||data.value.serverTimeText;const parsed=raw?new Date(String(raw).replace(' ','T')):new Date();serverBaseMs=Number.isNaN(parsed.getTime())?Date.now():parsed.getTime();syncedAtMs=Date.now();tick()}
|
||||
async function load(){if(!eventId||requesting)return;requesting=true;try{const res=await checkinScreenCastGet({eventId,_t:Date.now()});const body=res?.data??res;data.value=body?.data??body??{stages:[],units:[]};unitPage.value=Math.min(unitPage.value,unitPageCount.value-1);offline.value=false;syncClock()}catch{offline.value=true}finally{requesting=false}}
|
||||
function toggleFullscreen(){if(document.fullscreenElement)document.exitFullscreen?.().catch(()=>{});else document.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{});storesTagsViewRoutes.setCurrenFullscreen(true)}
|
||||
function toggleFullscreen(){if(fullscreenDocument.fullscreenElement)fullscreenDocument.exitFullscreen?.().catch(()=>{});else fullscreenDocument.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{});storesTagsViewRoutes.setCurrenFullscreen(true)}
|
||||
function requestAutoFullscreen(){if(route.query.autofullscreen==='1'&&!document.fullscreenElement)document.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{})}
|
||||
function showControls(){controlsVisible.value=true;window.clearTimeout(controlTimer);controlTimer=window.setTimeout(()=>controlsVisible.value=false,2600)}
|
||||
function setPreview(){if(!route.query.preview)return;const names=[['凌予蓁','刘姝妍'],['乔韵宁','廖歆悦'],['程沐晞','陈禹舟'],['韩蕙阳','孟清和'],['廖翊博','张景泽'],['任家驹','周瑾瑜'],['张锦川','施宜程'],['苏栩宁','林若瑶']];data.value={eventName:'2026年上海城市业余联赛跳绳联赛(杨浦站)',serverTimeText:'2026-07-29 10:05:32',weekDayText:'星期三',stages:[{eventProjectId:1,groupId:1,projectName:'3分钟10人长绳“8”字跳',groupName:'少儿男子组',stageStatus:1,stageStatusText:'检录中'},{eventProjectId:2,groupId:2,projectName:'两人同步规定套路',groupName:'少儿组',stageStatus:2,stageStatusText:'即将开始'},{eventProjectId:3,groupId:3,projectName:'个人花样基础套路赛',groupName:'儿童组',stageStatus:3,stageStatusText:'等待中'}],currentCheckin:{displayName:'3分钟10人长绳“8”字跳 · 少儿男子组'},currentCompeting:{displayName:'个人花样基础套路赛 · 儿童组',unitName:'跃动之夏培训学校有限公司',teamName:'诺宝宝花样跳绳队',athleteNames:['张嘉果','王灵曦','英轩诚']},units:names.map((athleteNames,index)=>({unitKey:String(index),athleteNames,teamName:`第 ${index+1} 参赛队`,checkinStatus:index<6?2:1,checkinStatusText:index<6?'已检录':'待检录'}))};syncClock()}
|
||||
|
||||
@ -35,7 +35,8 @@ const route=useRoute(),storesTagsViewRoutes=useTagsViewRoutes();
|
||||
let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-context')||'{}')}catch{savedContext={}}
|
||||
const eventId=Number(route.query.eventId)||Number(savedContext.eventId)||0;
|
||||
const venueId=route.query.venueId?Number(route.query.venueId):(Number(savedContext.venueId)||undefined);
|
||||
const cast=ref(null),offline=ref(false),controlsVisible=ref(true),rankPage=ref(0),fullscreenActive=ref(Boolean(document.fullscreenElement)),viewport=ref({width:window.innerWidth,height:window.innerHeight});
|
||||
const fullscreenDocument=route.query.presentation==='1'&&window.parent!==window?window.parent.document:document;
|
||||
const cast=ref(null),offline=ref(false),controlsVisible=ref(true),rankPage=ref(0),fullscreenActive=ref(Boolean(fullscreenDocument.fullscreenElement)),viewport=ref({width:window.innerWidth,height:window.innerHeight});
|
||||
let pollTimer,controlTimer,rankTimer,castChannel;
|
||||
function normalizePayload(value){if(Array.isArray(value))return value.map(normalizePayload);if(!value||typeof value!=='object')return value;return Object.fromEntries(Object.entries(value).map(([key,item])=>[key.charAt(0).toLowerCase()+key.slice(1),normalizePayload(item)]))}
|
||||
function unwrapApiData(response){let value=response;for(let i=0;i<3&&value&&typeof value==='object'&&Object.prototype.hasOwnProperty.call(value,'data');i++)value=value.data;return value}
|
||||
@ -67,12 +68,12 @@ const isBigScreen=computed(()=>storesTagsViewRoutes.isTagsViewCurrenFull),fullsc
|
||||
const screenScale=computed(()=>Math.min(viewport.value.width/1920,viewport.value.height/1080));
|
||||
let castRequesting=false;
|
||||
async function loadCast(){if(!eventId||castRequesting)return;castRequesting=true;try{const current=normalizeCast(unwrapApiData(await workbenchScreenCastCurrentGet({eventId,venueId,_t:Date.now()})));if(current)acceptPublishedCast({...current,displayKind:current.displayKind||route.query.displayKind});else if(!cast.value||Date.now()-Number(cast.value.cachedAt||0)>=30000)cast.value=null;offline.value=false}catch{offline.value=true}finally{castRequesting=false}}
|
||||
function toggleFullscreen(){if(document.fullscreenElement)document.exitFullscreen?.().catch(()=>{});else document.documentElement.requestFullscreen?.().catch(()=>{})}
|
||||
function onFullscreenChange(){fullscreenActive.value=Boolean(document.fullscreenElement)}
|
||||
function toggleFullscreen(){if(fullscreenDocument.fullscreenElement)fullscreenDocument.exitFullscreen?.().catch(()=>{});else fullscreenDocument.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{})}
|
||||
function onFullscreenChange(){fullscreenActive.value=Boolean(fullscreenDocument.fullscreenElement)}
|
||||
function onViewportResize(){viewport.value={width:window.innerWidth,height:window.innerHeight}}
|
||||
function showControls(){controlsVisible.value=true;window.clearTimeout(controlTimer);controlTimer=window.setTimeout(()=>controlsVisible.value=false,3200)}
|
||||
onMounted(()=>{storesTagsViewRoutes.setCurrenFullscreen(true);acceptPublishedCast(readCachedCast());if('BroadcastChannel' in window){castChannel=new BroadcastChannel(`yd-event-score-screen-${eventId}`);castChannel.onmessage=event=>acceptPublishedCast(event.data)}loadCast();pollTimer=window.setInterval(()=>document.visibilityState==='visible'&&loadCast(),3000);rankTimer=window.setInterval(()=>rankPage.value++,9000);document.addEventListener('fullscreenchange',onFullscreenChange);window.addEventListener('resize',onViewportResize);onViewportResize();showControls()});
|
||||
onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTimer);window.clearTimeout(controlTimer);castChannel?.close();document.removeEventListener('fullscreenchange',onFullscreenChange);window.removeEventListener('resize',onViewportResize)});
|
||||
onMounted(()=>{storesTagsViewRoutes.setCurrenFullscreen(true);acceptPublishedCast(readCachedCast());if('BroadcastChannel' in window){castChannel=new BroadcastChannel(`yd-event-score-screen-${eventId}`);castChannel.onmessage=event=>acceptPublishedCast(event.data)}loadCast();pollTimer=window.setInterval(()=>document.visibilityState==='visible'&&loadCast(),3000);rankTimer=window.setInterval(()=>rankPage.value++,9000);fullscreenDocument.addEventListener('fullscreenchange',onFullscreenChange);window.addEventListener('resize',onViewportResize);onViewportResize();showControls()});
|
||||
onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTimer);window.clearTimeout(controlTimer);castChannel?.close();fullscreenDocument.removeEventListener('fullscreenchange',onFullscreenChange);window.removeEventListener('resize',onViewportResize)});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -293,34 +293,114 @@ async function showCorrectionDetail(row){const res=await workbenchCorrectionDeta
|
||||
async function download(blob,name){const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
||||
async function exportResults(){const blob=await workbenchResultsExportGet({eventId:eventId.value,sessionId:selectedSession.value?.sessionId,keyword:entryKeyword.value||undefined,resultStatus:resultStatus.value??undefined});download(blob,`成绩册_${eventId.value}.xlsx`)}
|
||||
async function exportSheets(){const blob=await workbenchScoreSheetsExportGet({eventId:eventId.value,sessionId:selectedSession.value.sessionId});download(blob,`评分单_${selectedSession.value.projectName || ''}_${formatSessionName(selectedSession.value)}.xlsx`)}
|
||||
function scoreScreenHref(venueId,displayKind=castEntityKind.value,castMode='live'){return router.resolve({path:'/scoring/screen',query:{eventId:eventId.value,venueId,eventName:eventName.value,mode:'freestyle',castMode,displayKind,autofullscreen:'1'}}).href}
|
||||
function prepareScoreScreen(venueId){
|
||||
const existing=scoreScreenWindow&&!scoreScreenWindow.closed?scoreScreenWindow:null;
|
||||
const popup=existing||window.open('',scoreScreenWindowName,'popup=yes,width=1280,height=720,left=0,top=0');
|
||||
const presentationFrameId='yd-event-presentation-frame';
|
||||
function scoreScreenHref(venueId,displayKind=castEntityKind.value,castMode='live'){return router.resolve({path:'/scoring/screen',query:{eventId:eventId.value,venueId,eventName:eventName.value,mode:'freestyle',castMode,displayKind,autofullscreen:'1',presentation:'1'}}).href}
|
||||
function ensurePresentationShell(popup){
|
||||
const doc=popup.document;
|
||||
let frame=doc.getElementById(presentationFrameId);
|
||||
if(frame)return frame;
|
||||
doc.title='赛事大屏';
|
||||
doc.documentElement.style.cssText='width:100%;height:100%;margin:0;background:#020936;overflow:hidden';
|
||||
doc.body.style.cssText='width:100%;height:100%;margin:0;background:#020936;overflow:hidden';
|
||||
doc.body.replaceChildren();
|
||||
const loading=doc.createElement('div');
|
||||
loading.id='yd-event-presentation-loading';
|
||||
loading.textContent='正在准备投屏…';
|
||||
loading.style.cssText='position:fixed;inset:0;display:grid;place-items:center;color:#8feeff;background:#020936;font:28px Microsoft YaHei;letter-spacing:.08em';
|
||||
frame=doc.createElement('iframe');
|
||||
frame.id=presentationFrameId;
|
||||
frame.title='赛事投屏内容';
|
||||
frame.allow='fullscreen';
|
||||
frame.style.cssText='position:fixed;inset:0;width:100%;height:100%;border:0;background:#020936';
|
||||
const fullscreenButton=doc.createElement('button');
|
||||
fullscreenButton.id='yd-event-presentation-fullscreen';
|
||||
fullscreenButton.textContent='进入全屏';
|
||||
fullscreenButton.style.cssText='position:fixed;right:18px;top:18px;z-index:9;padding:10px 18px;border:1px solid rgba(95,220,255,.55);border-radius:10px;color:#d9faff;background:rgba(3,31,91,.88);font:600 14px Microsoft YaHei;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,.28)';
|
||||
const syncFullscreenButton=()=>{fullscreenButton.style.display=doc.fullscreenElement?'none':'block'};
|
||||
fullscreenButton.addEventListener('click',()=>requestPopupFullscreen(popup).then(syncFullscreenButton));
|
||||
doc.addEventListener('fullscreenchange',syncFullscreenButton);
|
||||
frame.addEventListener('load',()=>{
|
||||
loading.remove();
|
||||
const mode=frame.dataset.presentationMode;
|
||||
if(mode)frame.contentWindow?.postMessage({type:'yd-event-presentation-mode',mode},window.location.origin);
|
||||
});
|
||||
doc.body.append(loading,frame,fullscreenButton);
|
||||
syncFullscreenButton();
|
||||
return frame;
|
||||
}
|
||||
function reusablePresentationWindow(){
|
||||
if(!scoreScreenWindow||scoreScreenWindow.closed)return null;
|
||||
try{
|
||||
if(scoreScreenWindow.document.getElementById(presentationFrameId))return scoreScreenWindow;
|
||||
scoreScreenWindow.close();
|
||||
}catch{try{scoreScreenWindow.close()}catch{}}
|
||||
return null;
|
||||
}
|
||||
function requestPopupFullscreen(popup,targetScreen){
|
||||
try{
|
||||
if(popup.document.fullscreenElement)return Promise.resolve(true);
|
||||
const options={navigationUI:'hide'};
|
||||
if(targetScreen)options.screen=targetScreen;
|
||||
return popup.document.documentElement.requestFullscreen?.(options).then(()=>true).catch(()=>false);
|
||||
}catch{return Promise.resolve(false)}
|
||||
}
|
||||
function prepareScoreScreen(){
|
||||
if(window.electron?.presentation)return {electron:true};
|
||||
const existing=reusablePresentationWindow();
|
||||
const popup=existing||window.open('',scoreScreenWindowName,'popup=yes,fullscreen=yes,width=1280,height=720,left=0,top=0');
|
||||
if(!popup){ElMessage.warning('浏览器已拦截投屏窗口,请允许本站弹出窗口');return null}
|
||||
scoreScreenWindow=popup;
|
||||
const presentation={popup,created:!existing};
|
||||
if(presentation.created){try{popup.document.title='正在准备赛事大屏';popup.document.body.style.margin='0';popup.document.body.innerHTML='<div style="height:100vh;display:grid;place-items:center;background:#020a3c;color:#8feeff;font:28px Microsoft YaHei">正在准备投屏…</div>';popup.document.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{})}catch{} }
|
||||
placeOnPresentationScreen(popup);
|
||||
const frame=ensurePresentationShell(popup);
|
||||
const presentation={popup,frame,created:!existing};
|
||||
presentation.placementTask=placeOnPresentationScreen(popup);
|
||||
return presentation;
|
||||
}
|
||||
async function placeOnPresentationScreen(popup){
|
||||
try{if(popup.document.fullscreenElement){popup.focus();return true}}catch{}
|
||||
let target=window.screen;
|
||||
let fullscreenTarget;
|
||||
try{
|
||||
let target=window.screen;
|
||||
if('getScreenDetails' in window){const details=await window.getScreenDetails();target=details.screens.find(item=>item.left!==details.currentScreen.left||item.top!==details.currentScreen.top)||details.currentScreen||target}
|
||||
if('getScreenDetails' in window){
|
||||
const details=await window.getScreenDetails();
|
||||
target=details.screens.find(item=>item!==details.currentScreen&&(item.left!==details.currentScreen.left||item.top!==details.currentScreen.top))||details.currentScreen||target;
|
||||
fullscreenTarget=target;
|
||||
}
|
||||
popup.moveTo(target.availLeft??target.left??0,target.availTop??target.top??0);
|
||||
popup.resizeTo(target.availWidth??target.width,target.availHeight??target.height);
|
||||
popup.focus();
|
||||
}catch{try{popup.focus()}catch{}}
|
||||
let entered=await requestPopupFullscreen(popup,fullscreenTarget);
|
||||
if(!entered&&fullscreenTarget)entered=await requestPopupFullscreen(popup);
|
||||
return entered;
|
||||
}catch{try{popup.focus();return await requestPopupFullscreen(popup)}catch{return false}}
|
||||
}
|
||||
function showScoreScreen(presentation,venueId,displayKind=castEntityKind.value){
|
||||
const popup=presentation?.popup&&!presentation.popup.closed?presentation.popup:window.open(scoreScreenHref(venueId,displayKind),scoreScreenWindowName,'popup=yes,width=1280,height=720');
|
||||
if(!popup){ElMessage.warning('成绩已投出,但浏览器拦截了大屏窗口');return}
|
||||
scoreScreenWindow=popup;popup.onload=()=>{placeOnPresentationScreen(popup);try{popup.document.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{})}catch{}};popup.location.replace(scoreScreenHref(venueId,displayKind));placeOnPresentationScreen(popup);
|
||||
function showPresentationScreen(presentation,href){
|
||||
if(presentation?.electron&&window.electron?.presentation){
|
||||
const targetUrl=new URL(href,window.location.href).href;
|
||||
window.electron.presentation.open(targetUrl).catch(()=>ElMessage.error('无法打开 Electron 投屏窗口'));
|
||||
return;
|
||||
}
|
||||
const popup=presentation?.popup&&!presentation.popup.closed?presentation.popup:reusablePresentationWindow();
|
||||
if(!popup){ElMessage.warning('内容已投出,但浏览器拦截了大屏窗口');return}
|
||||
scoreScreenWindow=popup;
|
||||
const frame=presentation?.frame&&presentation.frame.isConnected?presentation.frame:ensurePresentationShell(popup);
|
||||
const targetUrl=new URL(href,window.location.href).href;
|
||||
const target=new URL(targetUrl);
|
||||
const mode=target.searchParams.get('castMode')==='checkin'?'checkin':'live';
|
||||
const scope=`${target.searchParams.get('eventId')||''}:${target.searchParams.get('venueId')||''}`;
|
||||
frame.dataset.presentationMode=mode;
|
||||
if(frame.dataset.presentationScope!==scope){
|
||||
frame.dataset.presentationScope=scope;
|
||||
frame.src=targetUrl;
|
||||
}else{
|
||||
frame.contentWindow?.postMessage({type:'yd-event-presentation-mode',mode},window.location.origin);
|
||||
}
|
||||
popup.focus();
|
||||
if(!presentation?.placementTask&&!popup.document.fullscreenElement)requestPopupFullscreen(popup);
|
||||
}
|
||||
function showScoreScreen(presentation,venueId,displayKind=castEntityKind.value){showPresentationScreen(presentation,scoreScreenHref(venueId,displayKind,'live'))}
|
||||
function closeUnusedScreen(presentation){if(presentation?.created&&presentation.popup&&!presentation.popup.closed)presentation.popup.close()}
|
||||
function openScoreScreen(){rememberCastKind();const presentation=prepareScoreScreen(selectedSession.value?.venueId);if(presentation)showScoreScreen(presentation,selectedSession.value?.venueId,castEntityKind.value)}
|
||||
function openCheckinScreen(){const venueId=selectedSession.value?.venueId;const presentation=prepareScoreScreen(venueId);if(!presentation)return;const popup=presentation.popup;popup.onload=()=>{placeOnPresentationScreen(popup);try{popup.document.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{})}catch{}};popup.location.replace(scoreScreenHref(venueId,castEntityKind.value,'checkin'));placeOnPresentationScreen(popup)}
|
||||
function openCheckinScreen(){const venueId=selectedSession.value?.venueId;const presentation=prepareScoreScreen();if(presentation)showPresentationScreen(presentation,scoreScreenHref(venueId,castEntityKind.value,'checkin'))}
|
||||
function goLauncher(){router.push({path:'/scoring/launcher'})}
|
||||
function toggleCorrections(){activeTab.value=activeTab.value==='corrections'?'results':'corrections';if(activeTab.value==='corrections')loadCorrections()}
|
||||
function startPolling(){pollTimer=window.setInterval(()=>{if(document.visibilityState==='visible'){loadCurrentCast();if(activeTab.value!=='corrections'&&!batchConfirmDialogOpen.value)loadEntries(true)}},5000)}
|
||||
|
||||
@ -25,8 +25,19 @@ const getBuildTimeDirectory = () => {
|
||||
const viteConfig = defineConfig((mode: ConfigEnv) => {
|
||||
const env = loadEnv(mode.mode, process.cwd());
|
||||
const outDir = mode.command === 'build' ? process.env.BUILD_OUT_DIR || getBuildTimeDirectory() : 'dist';
|
||||
const electronApiTarget =
|
||||
process.env.ELECTRON_API_ENV === 'test'
|
||||
? process.env.ELECTRON_TEST_API_URL || 'http://8.153.108.90:9996'
|
||||
: process.env.ELECTRON_PRODUCTION_API_URL || process.env.ELECTRON_API_URL || 'https://event.yuedongsports.com';
|
||||
return {
|
||||
plugins: [vue(), vueJsx(), vueSetupExtend(), viteCompression(), JSON.parse(env.VITE_OPEN_CDN) ? buildConfig.cdn() : null],
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
vueSetupExtend(),
|
||||
// Electron 从本地资源包读取文件,不需要生成 gzip 副本;同时规避 Windows 下压缩插件产生绝对路径目录。
|
||||
mode.mode === 'electron' ? null : viteCompression(),
|
||||
JSON.parse(env.VITE_OPEN_CDN) ? buildConfig.cdn() : null,
|
||||
],
|
||||
root: process.cwd(),
|
||||
resolve: { alias },
|
||||
base: mode.command === 'serve' ? './' : process.env.BUILD_PUBLIC_PATH || env.VITE_PUBLIC_PATH,
|
||||
@ -41,7 +52,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_TARGET || 'https://event.yuedongsports.com',
|
||||
target: mode.mode === 'electron' ? electronApiTarget : env.VITE_API_TARGET || 'https://event.yuedongsports.com',
|
||||
// target: 'http://8.153.108.90:9996',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user