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