修复单屏发布闪烁+编辑界面增强+新增页面默认值优化
- 修复单屏发布/轮播切页屏幕闪烁与任务栏闪现(所有窗体创建即预设全屏,切换仅 show/hide/focus,先隐旧再显新) - 轮播取消循环:越界保持当前页并在左上角提示,自动轮播到边界即停 - 编辑窗体:预览区地址栏新增复制按钮、页面属性 URL 输入框加高两行 - 配置窗体:工程路径新增复制按钮、去掉工具按钮组与桌面快捷方式入口 - 新增/插入页面默认值:URL/参数/描述留空,所属屏幕与控制器编号取当前最大值+1
This commit is contained in:
@@ -286,20 +286,23 @@ function setDirtyFlag(d: boolean): void {
|
||||
|
||||
export function installCloseGuard(win: BrowserWindow): void {
|
||||
win.on('close', (e) => {
|
||||
if (!dirty || forceQuit) return
|
||||
if (forceQuit) return
|
||||
e.preventDefault()
|
||||
const warn = dirty ? '当前工程有未保存的修改。\n\n' : ''
|
||||
void dialog
|
||||
.showMessageBox(win, {
|
||||
type: 'warning',
|
||||
title: '工程尚未保存',
|
||||
message: '当前工程有未保存的修改,确定要关闭吗?',
|
||||
buttons: ['仍然关闭', '取消'],
|
||||
title: '确认退出',
|
||||
message: `${warn}确定要退出智慧农业大数据可视化控制中心吗?`,
|
||||
buttons: ['退出', '取消'],
|
||||
defaultId: 1,
|
||||
cancelId: 1
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.response === 0) {
|
||||
forceQuit = true
|
||||
// 先关闭所有播放中的子页面(屏幕窗体/轮播/探测/主屏播放),再关闭主窗口
|
||||
closeAllScreens()
|
||||
win.close()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -150,6 +150,26 @@ function lockHint(win: BrowserWindow | null, locked: boolean): void {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
/** 在窗体页面左上角显示临时提示(2 秒后自动移除) */
|
||||
function hintAt(win: BrowserWindow, text: string): void {
|
||||
if (!win || win.isDestroyed()) return
|
||||
const script = `(() => {
|
||||
const old = document.getElementById('__sb_hint_root')
|
||||
if (old) old.remove()
|
||||
const d = document.createElement('div')
|
||||
d.id = '__sb_hint_root'
|
||||
d.textContent = ${JSON.stringify(text)}
|
||||
d.style.cssText =
|
||||
'position:fixed;top:16px;left:16px;z-index:2147483647;padding:8px 16px;' +
|
||||
'border-radius:6px;background:rgba(0,0,0,.65);color:#fff;' +
|
||||
'font:14px/1.6 "Microsoft YaHei",sans-serif;pointer-events:none;' +
|
||||
'box-shadow:0 2px 8px rgba(0,0,0,.3);'
|
||||
document.documentElement.appendChild(d)
|
||||
setTimeout(() => d.remove(), 2000)
|
||||
})()`
|
||||
void win.webContents.executeJavaScript(script).catch(() => undefined)
|
||||
}
|
||||
|
||||
export function setPages(pages: SyncPage[]): void {
|
||||
lastPages = pages
|
||||
}
|
||||
@@ -337,7 +357,7 @@ export function startCarousel(index: number, pages: CarouselPage[], intervalSec
|
||||
frame: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: '#050b18',
|
||||
show: pi === 0,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
@@ -348,13 +368,21 @@ export function startCarousel(index: number, pages: CarouselPage[], intervalSec
|
||||
win.setMenuBarVisibility(false)
|
||||
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
||||
win.setTitle(page.title)
|
||||
// 全部窗体预先设为全屏:避免首次切到时先从窗口化进入全屏,产生过渡动画/任务栏闪现
|
||||
win.setFullScreen(true)
|
||||
void win.loadURL(page.url)
|
||||
|
||||
// 全部窗体就绪后保持显示并叠放:任一时刻最顶层都有全屏窗体覆盖任务栏,
|
||||
// 切换只改置顶顺序不做 hide/show,从根源避免任务栏闪现
|
||||
win.once('ready-to-show', () => {
|
||||
if (pi === 0 && !win.isDestroyed()) {
|
||||
win.show()
|
||||
win.setFullScreen(true)
|
||||
win.focus()
|
||||
if (win.isDestroyed()) return
|
||||
win.show()
|
||||
if (!win.isFullScreen()) win.setFullScreen(true)
|
||||
const st = carousels.get(index)
|
||||
const first = st?.wins[0]
|
||||
if (first && !first.isDestroyed() && first !== win) {
|
||||
first.moveTop()
|
||||
first.focus()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -418,8 +446,9 @@ export function startCarousel(index: number, pages: CarouselPage[], intervalSec
|
||||
const st = carousels.get(index)
|
||||
const first = st?.wins[0]
|
||||
if (st && first && !first.isDestroyed()) {
|
||||
first.show()
|
||||
first.setFullScreen(true)
|
||||
if (!first.isVisible()) first.show()
|
||||
if (!first.isFullScreen()) first.setFullScreen(true)
|
||||
first.moveTop()
|
||||
first.focus()
|
||||
}
|
||||
}, 400)
|
||||
@@ -449,25 +478,38 @@ export function startInteractive(req: { mainIndex: number; pagesByScreen: Record
|
||||
return listScreens()
|
||||
}
|
||||
|
||||
/** 切换到第 next 个窗体(循环),只显示目标窗体 */
|
||||
/** 切换到第 next 个窗体(不循环),越界时保持当前页并在左上角提示 */
|
||||
function switchCarousel(index: number, next: number): void {
|
||||
const state = carousels.get(index)
|
||||
if (!state) return
|
||||
const n = state.wins.length
|
||||
if (n === 0) return
|
||||
state.current = ((next % n) + n) % n
|
||||
// 取消循环播放:到第一屏/最后一屏时不再回绕,仅提示
|
||||
if (next < 0 || next >= n) {
|
||||
const w = state.wins[state.current]
|
||||
if (w && !w.isDestroyed()) hintAt(w, next < 0 ? '当前已经是第一屏' : '当前已经是最后一屏')
|
||||
// 自动轮播播到边界后停止定时切换,避免反复提示
|
||||
if (state.auto && state.timer) {
|
||||
clearInterval(state.timer)
|
||||
state.timer = null
|
||||
state.auto = false
|
||||
}
|
||||
return
|
||||
}
|
||||
state.current = next
|
||||
// 全屏窗体同尺寸完全叠放:切换只把目标窗体置顶并聚焦,不做 hide/show,
|
||||
// 使最顶层始终有全屏窗体覆盖任务栏,避免切页瞬间任务栏闪现
|
||||
state.wins.forEach((w, i) => {
|
||||
if (w.isDestroyed()) return
|
||||
if (i === state.current) {
|
||||
w.show()
|
||||
w.setFullScreen(true)
|
||||
if (!w.isVisible()) w.show()
|
||||
if (!w.isFullScreen()) w.setFullScreen(true)
|
||||
w.moveTop()
|
||||
w.focus()
|
||||
// 切换后短暂延迟再聚焦一次,确保键盘输入落在当前窗体
|
||||
setTimeout(() => {
|
||||
if (!w.isDestroyed() && w.isVisible()) w.focus()
|
||||
}, 120)
|
||||
} else {
|
||||
w.hide()
|
||||
}
|
||||
})
|
||||
emitChange()
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useStore } from '../store'
|
||||
import type { ViewMode } from '../types'
|
||||
import { CONFIG_MENUS, type ConfigMenu } from '../configMenus'
|
||||
import { closeAllScreenOutput, publishCurrentToScreen, startCarouselOnScreen, stopCarouselAutoOnScreen } from '../publishActions'
|
||||
import { toast } from '../toast'
|
||||
import { ScreenSelectModal, MultiPublishModal, InteractiveModal, DetectModal } from './PublishModals'
|
||||
|
||||
interface RibbonItem {
|
||||
@@ -229,15 +228,6 @@ export default function Ribbon(props: {
|
||||
}
|
||||
}))
|
||||
)}
|
||||
{renderGroup('工具', [
|
||||
{
|
||||
id: 'shortcut',
|
||||
icon: '🖱️',
|
||||
label: '桌面快捷方式',
|
||||
action: () =>
|
||||
void window.api.createShortcut().then((ok) => toast(ok ? '已创建桌面快捷方式' : '创建快捷方式失败', ok ? 'ok' : 'warn'))
|
||||
}
|
||||
])}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,16 @@ export default function ProjectInfo(): React.JSX.Element {
|
||||
toast('项目信息与 JSON 密码已保存', 'ok')
|
||||
}
|
||||
|
||||
const copyPath = async (): Promise<void> => {
|
||||
if (!filePath) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(filePath)
|
||||
toast('工程路径已复制', 'ok')
|
||||
} catch {
|
||||
toast('复制失败,请手动选择复制', 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cfg-section">
|
||||
<div className="cfg-card">
|
||||
@@ -81,8 +91,13 @@ export default function ProjectInfo(): React.JSX.Element {
|
||||
<div className="cfg-card-body">
|
||||
<div className="cfg-row">
|
||||
<span className="cfg-row-label">工程路径</span>
|
||||
<span className="cfg-row-value" title={filePath ?? ''}>
|
||||
{filePath || '未保存(暂存于内存)'}
|
||||
<span className="cfg-path">
|
||||
<span className="cfg-row-value" title={filePath ?? ''}>
|
||||
{filePath || '未保存(暂存于内存)'}
|
||||
</span>
|
||||
<button className="btn" disabled={!filePath} onClick={() => void copyPath()}>
|
||||
复制
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="cfg-row">
|
||||
|
||||
@@ -38,6 +38,22 @@ interface AppState {
|
||||
markSaved: () => void
|
||||
}
|
||||
|
||||
/** 新增页面默认值:屏幕/控制器编号取当前最大值 +1,URL/参数/描述留空 */
|
||||
function blankPage(pages: Page[], id: string, title: string): Page {
|
||||
const maxScreen = pages.reduce((m, p) => Math.max(m, Number(p.screen) || 0), 0)
|
||||
const maxCtrl = pages.reduce((m, p) => Math.max(m, Number(p.ctrl) || 0), 0)
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
desc: '',
|
||||
screen: maxScreen + 1,
|
||||
ctrl: maxCtrl + 1,
|
||||
display: true,
|
||||
url: '',
|
||||
params: ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用工程 JSON 到编辑状态 */
|
||||
function applyProject(get: () => AppState, set: (p: Partial<AppState>) => void, path: string, json: ProjectJson): void {
|
||||
const pages = jsonToPages(json)
|
||||
@@ -78,16 +94,7 @@ export const useStore = create<AppState>((set, get) => {
|
||||
addPage: () => {
|
||||
const pages = get().pages
|
||||
const id = nextPageId(pages)
|
||||
const page: Page = {
|
||||
id,
|
||||
title: '新页面 ' + pages.length + 1,
|
||||
desc: '新建页面',
|
||||
screen: 1,
|
||||
ctrl: 0,
|
||||
display: true,
|
||||
url: 'sample-screen.html',
|
||||
params: `no=${id.replace(/\D/g, '')}\ntitle=${id}`
|
||||
}
|
||||
const page = blankPage(pages, id, '新页面 ' + (pages.length + 1))
|
||||
const list = [...pages, page]
|
||||
touch({ pages: list, curIdx: list.length - 1 })
|
||||
toast('已新建页面 ' + id, 'ok')
|
||||
@@ -96,16 +103,7 @@ export const useStore = create<AppState>((set, get) => {
|
||||
const { pages, curIdx } = get()
|
||||
const at = index ?? curIdx
|
||||
const id = nextPageId(pages)
|
||||
const page: Page = {
|
||||
id,
|
||||
title: '新页面 ' + pages.length + 1,
|
||||
desc: '新建页面',
|
||||
screen: 1,
|
||||
ctrl: 0,
|
||||
display: true,
|
||||
url: 'sample-screen.html',
|
||||
params: `no=${id.replace(/\D/g, '')}\ntitle=${id}`
|
||||
}
|
||||
const page = blankPage(pages, id, '新页面 ' + (pages.length + 1))
|
||||
const list = [...pages]
|
||||
list.splice(at, 0, page)
|
||||
touch({ pages: list, curIdx: at })
|
||||
|
||||
@@ -1282,6 +1282,17 @@ select {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cfg-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cfg-path .cfg-row-value {
|
||||
max-width: 70%;
|
||||
}
|
||||
.cfg-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useMemo, useRef } from 'react'
|
||||
import { useStore } from '../store'
|
||||
import { pageSrc, resolvePageUrl } from '../utils'
|
||||
import WebFrame, { type WebFrameHandle } from '../components/WebFrame'
|
||||
import { IconCopy } from '../components/icons'
|
||||
import { toast } from '../toast'
|
||||
|
||||
export default function Canvas(): React.JSX.Element {
|
||||
const page = useStore((s) => s.pages[s.curIdx])
|
||||
@@ -13,6 +15,16 @@ export default function Canvas(): React.JSX.Element {
|
||||
const baseUrl = useMemo(() => (page ? resolvePageUrl(page) : ''), [page])
|
||||
const isBlank = src === 'about:blank'
|
||||
|
||||
const copyUrl = async (): Promise<void> => {
|
||||
if (!baseUrl) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(baseUrl)
|
||||
toast('地址已复制', 'ok')
|
||||
} catch {
|
||||
toast('复制失败,请手动选择复制', 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="canvas-toolbar">
|
||||
@@ -22,6 +34,9 @@ export default function Canvas(): React.JSX.Element {
|
||||
<div className="ct-url" title={baseUrl}>
|
||||
{baseUrl}
|
||||
</div>
|
||||
<button className="icon-btn" title="复制地址" disabled={isBlank} onClick={() => void copyUrl()}>
|
||||
<IconCopy size={13} />
|
||||
</button>
|
||||
<button
|
||||
className="icon-btn"
|
||||
title="刷新大屏页面"
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function PropertyPanel(): React.JSX.Element {
|
||||
<label>大屏页面 URL(PageUrl)</label>
|
||||
<textarea
|
||||
className="url-area"
|
||||
rows={3}
|
||||
rows={5}
|
||||
value={page.url}
|
||||
onChange={(e) => updatePage({ url: e.target.value })}
|
||||
onPaste={(e) => {
|
||||
|
||||
Reference in New Issue
Block a user