From e0d96051f1024a80e0574195c66b1a84fd76376a Mon Sep 17 00:00:00 2001 From: fanhongcai Date: Sun, 23 Aug 2026 17:47:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=95=E5=B1=8F=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E9=97=AA=E7=83=81+=E7=BC=96=E8=BE=91=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=A2=9E=E5=BC=BA+=E6=96=B0=E5=A2=9E=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=BB=98=E8=AE=A4=E5=80=BC=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复单屏发布/轮播切页屏幕闪烁与任务栏闪现(所有窗体创建即预设全屏,切换仅 show/hide/focus,先隐旧再显新) - 轮播取消循环:越界保持当前页并在左上角提示,自动轮播到边界即停 - 编辑窗体:预览区地址栏新增复制按钮、页面属性 URL 输入框加高两行 - 配置窗体:工程路径新增复制按钮、去掉工具按钮组与桌面快捷方式入口 - 新增/插入页面默认值:URL/参数/描述留空,所属屏幕与控制器编号取当前最大值+1 --- .codebuddy/memory/2026-08-23.md | 6 ++ SmartAgriCenter/src/main/ipc.ts | 11 +-- SmartAgriCenter/src/main/screens.ts | 68 +++++++++++++++---- .../src/renderer/src/components/Ribbon.tsx | 10 --- .../src/components/config/ProjectInfo.tsx | 19 +++++- SmartAgriCenter/src/renderer/src/store.ts | 38 +++++------ .../src/renderer/src/styles/global.css | 11 +++ .../src/renderer/src/views/Canvas.tsx | 15 ++++ .../src/renderer/src/views/PropertyPanel.tsx | 2 +- 9 files changed, 130 insertions(+), 50 deletions(-) diff --git a/.codebuddy/memory/2026-08-23.md b/.codebuddy/memory/2026-08-23.md index 27e74f2..3ebe4bc 100644 --- a/.codebuddy/memory/2026-08-23.md +++ b/.codebuddy/memory/2026-08-23.md @@ -237,3 +237,9 @@ - **发布组 4 按钮图标统一**:预览本页/单屏发布/多屏发布/交互发布 由 emoji 换成**统一样式线性 SVG**(显示器族:24 viewBox、stroke currentColor 1.8、15px、在 `.rb-icon` flex 容器内自动居中着色):预览=显示器+播放三角、单屏=显示器+轮播旋转箭头、多屏=前后双显示器、交互=显示器+双向箭头。`Ribbon.tsx` 顶部定义 `svgIconProps` + 4 个图标组件,`RibbonItem.icon` 类型改为 `string | React.JSX.Element`(emoji 字符串兼容)。CDP 实测发布组 `svgs=4`。 - **属性面板**:URL 输入框 `rows 2→3`;页面参数 textarea `rows=4`(默认 2 加 2 行)并**删除 placeholder 水印**(URL 的 placeholder 保留)。`.prop-item textarea` 仅 `min-height:46px`+`resize:vertical`,无固定高度,rows 生效。 - **CDP 验证 build 产物**注意:`npm run build` 后必须用 `node_modules\electron\dist\electron.exe . --remote-debugging-port=9223`(`electron.cmd` 不在 PATH 会报 file not found),页面 URL 是 `file://...out/renderer/index.html`,脚本匹配 page 类型须排除 chrome:// 与 devtools。验证完已杀实例重启 dev 至 5173。 +- **已提交并推送**:commit `aa8f070`「v0.2.0: 交互发布+多屏发布增强+页面管理优化」(22 files, +657/-134),`git push` 成功 `8d96f51..aa8f070 main -> main`(github.bbitcn.net/fanhongcai/DataViewCenter.git)。提交信息用 `git commit -F`(UTF-8 文件,规避 GBK 乱码)。推送时出现 `credential-manager-core is not a git command` 警告为无害提示(GCM 凭据已缓存)。 +- **UI 调整 4 项(未提交)**:① 编辑窗体预览区地址栏 `.ct-url` 右侧新增复制地址按钮(`IconCopy` + `navigator.clipboard` + toast,Canvas.tsx);② 页面属性「大屏页面 URL」textarea rows 3→5(PropertyPanel.tsx);③ 配置窗体「当前工程」工程路径行新增「复制」按钮(`.cfg-path` flex 布局,ProjectInfo.tsx);④ Ribbon 配置视图删除「工具」组(含桌面快捷方式按钮),renderer 不再引用 `window.api.createShortcut`(preload/IPC 保留未删,ShortcutPanel 配置菜单页仍可用)。 +- **修复单屏发布切页闪烁/任务栏闪现(screens.ts,未提交)**:第一版(预设全屏+先 hide 后 show+防御性 setFullScreen)用户实测无效。**第二版彻底方案(已实施)**:Windows 上 hide 全屏窗体本身就会导致任务栏重排闪现,故轮播全部窗体就绪后**保持显示叠放、永不 hide**;切换只 `moveTop()+focus()` 把目标窗体置顶(同尺寸全屏窗体完全叠放),最顶层始终有全屏窗体覆盖任务栏。ready-to-show 任一窗体就绪即 show,并把 `wins[0]` 置顶。400ms/120ms 兜底 focus 同步改为 moveTop 语义。经验:**Windows Electron 多全屏窗体切换禁用 hide/show,用 moveTop 置顶切换**。 +- **主程序关闭确认+子页面联动退出(ipc.ts,未提交)**:installCloseGuard 改为统一弹「确认退出」对话框(合并未保存警告),确认后先 `closeAllScreens()` 再 `win.close()` → `window-all-closed` → `app.quit()`(before-quit 里 closeAllScreens 幂等无副作用)。此前仅 dirty 时弹「工程尚未保存」,且主窗关后子窗口仍存活 app 不退出。 +- **轮播取消循环+边界左上角提示(screens.ts,未提交)**:switchCarousel 去掉 `%n` 取模回绕,越界(next<0 或 next>=n)时保持当前页,并新增 `hintAt(win,text)` 在页面左上角注入 DOM overlay(position:fixed 深色圆角小条,2 秒自动移除,`executeJavaScript` 注入,不受页面 CSP 限制)提示「当前已经是第一屏/最后一屏」;自动轮播播到边界后 clearInterval 并置 `state.auto=false`(避免反复提示)。**单屏轮播与交互发布主屏共用 switchCarousel,因此两者同时生效**。 +- **新增/插入页面默认值调整(store.ts,未提交)**:新增 `blankPage()` 辅助函数:url=''、params=''、desc='' 全留空;screen/ctrl 取现有页面**最大值 +1**(需求更正:先是"取最大值",用户又改为"最大值+1")。addPage 与 insertBefore 共用,同时修正标题拼接 bug `'新页面 ' + pages.length + 1`(字符串拼接得到"新页面 41")→ `(pages.length + 1)`。空 url 经 `pageSrc`/`resolvePageUrl` 安全返回 about:blank。renderer 改动 HMR 生效。 diff --git a/SmartAgriCenter/src/main/ipc.ts b/SmartAgriCenter/src/main/ipc.ts index 0db1d3e..8c38b86 100644 --- a/SmartAgriCenter/src/main/ipc.ts +++ b/SmartAgriCenter/src/main/ipc.ts @@ -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() } }) diff --git a/SmartAgriCenter/src/main/screens.ts b/SmartAgriCenter/src/main/screens.ts index 2441b7d..4561bea 100644 --- a/SmartAgriCenter/src/main/screens.ts +++ b/SmartAgriCenter/src/main/screens.ts @@ -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() diff --git a/SmartAgriCenter/src/renderer/src/components/Ribbon.tsx b/SmartAgriCenter/src/renderer/src/components/Ribbon.tsx index e1e6d31..63d0a86 100644 --- a/SmartAgriCenter/src/renderer/src/components/Ribbon.tsx +++ b/SmartAgriCenter/src/renderer/src/components/Ribbon.tsx @@ -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')) - } - ])} )} diff --git a/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx b/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx index dbce72f..4409396 100644 --- a/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx +++ b/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx @@ -31,6 +31,16 @@ export default function ProjectInfo(): React.JSX.Element { toast('项目信息与 JSON 密码已保存', 'ok') } + const copyPath = async (): Promise => { + if (!filePath) return + try { + await navigator.clipboard.writeText(filePath) + toast('工程路径已复制', 'ok') + } catch { + toast('复制失败,请手动选择复制', 'warn') + } + } + return (
@@ -81,8 +91,13 @@ export default function ProjectInfo(): React.JSX.Element {
工程路径 - - {filePath || '未保存(暂存于内存)'} + + + {filePath || '未保存(暂存于内存)'} + +
diff --git a/SmartAgriCenter/src/renderer/src/store.ts b/SmartAgriCenter/src/renderer/src/store.ts index b4536db..e8c2abc 100644 --- a/SmartAgriCenter/src/renderer/src/store.ts +++ b/SmartAgriCenter/src/renderer/src/store.ts @@ -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) => void, path: string, json: ProjectJson): void { const pages = jsonToPages(json) @@ -78,16 +94,7 @@ export const useStore = create((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((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 }) diff --git a/SmartAgriCenter/src/renderer/src/styles/global.css b/SmartAgriCenter/src/renderer/src/styles/global.css index 927de37..beefd24 100644 --- a/SmartAgriCenter/src/renderer/src/styles/global.css +++ b/SmartAgriCenter/src/renderer/src/styles/global.css @@ -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; diff --git a/SmartAgriCenter/src/renderer/src/views/Canvas.tsx b/SmartAgriCenter/src/renderer/src/views/Canvas.tsx index 0cc6217..0828900 100644 --- a/SmartAgriCenter/src/renderer/src/views/Canvas.tsx +++ b/SmartAgriCenter/src/renderer/src/views/Canvas.tsx @@ -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 => { + if (!baseUrl) return + try { + await navigator.clipboard.writeText(baseUrl) + toast('地址已复制', 'ok') + } catch { + toast('复制失败,请手动选择复制', 'warn') + } + } + return ( <>
@@ -22,6 +34,9 @@ export default function Canvas(): React.JSX.Element {
{baseUrl}
+