v1.5.1: 文传易外链+缩略图改截预览webview+工程整体加密+缩略图压缩
- 配置按钮组新增「文传易」外链(关于之前,系统浏览器打开 http://wenchuanyi.bbitcn.net/) - 缩略图改为对编辑区预览 webview 直接截图(所见即所得),非当前预览页回退按 URL 截图 - 工程 JSON 整体加密(AES-256-GCM,enc:v1: 覆盖整个 JSON) - 缩略图压缩 ≤500KB(PNG→JPEG) - 版本号升至 1.5.1(同步 settings/utils/store/ProjectInfo/AboutPanel 默认值) - 更新开发进度存档与 MEMORY 至 v1.5.1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "smart-agri-center",
|
||||
"version": "0.2.0",
|
||||
"version": "1.5.1",
|
||||
"description": "智慧农业大数据可视化控制中心(Electron + React + TypeScript)",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "FanHongCai",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app, dialog, ipcMain, shell, BrowserWindow } from 'electron'
|
||||
import { app, clipboard, dialog, ipcMain, nativeImage, shell, BrowserWindow } from 'electron'
|
||||
import type { NativeImage } from 'electron'
|
||||
import { exec } from 'child_process'
|
||||
import { dirname, join } from 'path'
|
||||
import {
|
||||
@@ -35,7 +36,45 @@ function beepSystem(): void {
|
||||
exec('powershell -NoProfile -NonInteractive -Command "[console]::beep(880,400)"', () => undefined)
|
||||
}
|
||||
|
||||
/** 截图压缩为 JPEG dataURL,保证体积不超过 maxBytes(默认 500KB):先降质量,再逐步缩小尺寸 */
|
||||
function compressSnapshot(img: NativeImage, maxBytes = 500 * 1024): string {
|
||||
let cur = img
|
||||
const fit = (side: number): void => {
|
||||
const { width, height } = cur.getSize()
|
||||
const maxSide = Math.max(width, height)
|
||||
if (maxSide > side) {
|
||||
const k = side / maxSide
|
||||
cur = cur.resize({ width: Math.max(1, Math.round(width * k)), height: Math.max(1, Math.round(height * k)) })
|
||||
}
|
||||
}
|
||||
const toData = (q: number): { buf: Buffer; url: string } => {
|
||||
const buf = cur.toJPEG(q)
|
||||
return { buf, url: 'data:image/jpeg;base64,' + buf.toString('base64') }
|
||||
}
|
||||
fit(1600)
|
||||
for (const q of [80, 65, 50, 35]) {
|
||||
const { buf, url } = toData(q)
|
||||
if (buf.length <= maxBytes) return url
|
||||
}
|
||||
for (const side of [1280, 960, 640, 480]) {
|
||||
fit(side)
|
||||
for (const q of [60, 45, 30]) {
|
||||
const { buf, url } = toData(q)
|
||||
if (buf.length <= maxBytes) return url
|
||||
}
|
||||
}
|
||||
return toData(25).url
|
||||
}
|
||||
|
||||
export function registerIpc(): void {
|
||||
// ===== 剪贴板写入(主进程 clipboard 保证可靠,渲染层 navigator.clipboard 易受权限/焦点影响) =====
|
||||
ipcMain.handle('clipboard:write', (_e, text: string): void => {
|
||||
clipboard.writeText(String(text ?? ''))
|
||||
})
|
||||
|
||||
// ===== 系统浏览器打开外部链接 =====
|
||||
ipcMain.handle('shell:openExternal', (_e, url: string): Promise<void> => shell.openExternal(url))
|
||||
|
||||
// ===== 窗口控制 =====
|
||||
ipcMain.on('win:minimize', () => BrowserWindow.getFocusedWindow()?.minimize())
|
||||
ipcMain.on('win:toggle-maximize', () => {
|
||||
@@ -160,6 +199,16 @@ export function registerIpc(): void {
|
||||
ipcMain.handle('app:consumeLoad', () => consumePendingLoad())
|
||||
|
||||
// ===== 页面截图(预览图)=====
|
||||
// 预览 webview 截图 → PNG Buffer → 主进程压缩为 ≤500KB 的 JPEG dataURL
|
||||
ipcMain.handle('page:compress', (_e, png: Uint8Array) => {
|
||||
try {
|
||||
const img = nativeImage.createFromBuffer(Buffer.from(png))
|
||||
if (img.isEmpty()) return null
|
||||
return compressSnapshot(img)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
ipcMain.handle('page:snapshot', async (_e, url: string) => {
|
||||
if (!url || url === 'about:blank') return null
|
||||
const win = new BrowserWindow({
|
||||
@@ -181,7 +230,7 @@ export function registerIpc(): void {
|
||||
await new Promise((r) => setTimeout(r, 1600))
|
||||
const img = await win.webContents.capturePage()
|
||||
if (img.isEmpty()) return null
|
||||
return img.toDataURL()
|
||||
return compressSnapshot(img)
|
||||
} catch {
|
||||
return null
|
||||
} finally {
|
||||
|
||||
@@ -48,12 +48,27 @@ export function pagesToSync(json: ProjectJson): SyncPage[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取工程文件并解密敏感字段。
|
||||
* 读取工程文件。
|
||||
* - 整体加密文件(内容以 enc:v1: 开头):用密码整体解密后解析
|
||||
* - 旧格式(明文 / 仅字段加密):逐字段解密
|
||||
* 密码错误时返回 error='密码错误'
|
||||
*/
|
||||
export async function readProjectFile(path: string, password: string): Promise<ReadResult> {
|
||||
try {
|
||||
const raw = await fs.readFile(path, 'utf8')
|
||||
// 整体加密:encryptText 输出的前缀是 enc:v1:
|
||||
if (raw.startsWith('enc:v1:')) {
|
||||
const dec = decryptText(raw, password)
|
||||
if (dec === null) return { json: null, path, error: '密码错误' }
|
||||
let json: ProjectJson
|
||||
try {
|
||||
json = JSON.parse(dec) as ProjectJson
|
||||
} catch {
|
||||
return { json: null, path, error: 'JSON 文件无法解析' }
|
||||
}
|
||||
return { json, path }
|
||||
}
|
||||
// 旧格式:明文 JSON,敏感字段可能为 enc:v1: 前缀密文
|
||||
const json = JSON.parse(raw) as ProjectJson
|
||||
if (Array.isArray(json.pages)) {
|
||||
for (const p of json.pages) {
|
||||
@@ -75,24 +90,14 @@ export async function readProjectFile(path: string, password: string): Promise<R
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存工程文件;设置了密码时对 URL / 参数加密存储 */
|
||||
/** 保存工程文件;设置了密码时对整个 JSON 内容加密存储 */
|
||||
export async function writeProjectFile(
|
||||
path: string,
|
||||
data: string,
|
||||
password: string
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
try {
|
||||
let out = data
|
||||
if (password) {
|
||||
const json = JSON.parse(data) as ProjectJson
|
||||
if (Array.isArray(json.pages)) {
|
||||
for (const p of json.pages) {
|
||||
if (typeof p.PageUrl === 'string') p.PageUrl = encryptText(p.PageUrl, password)
|
||||
if (typeof p.PageParams === 'string') p.PageParams = encryptText(p.PageParams, password)
|
||||
}
|
||||
}
|
||||
out = JSON.stringify(json, null, 2)
|
||||
}
|
||||
const out = password ? encryptText(data, password) : data
|
||||
await fs.writeFile(path, out, 'utf8')
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface AppSettings {
|
||||
|
||||
const DEFAULTS: AppSettings = {
|
||||
projectName: '智慧农业大数据可视化控制中心',
|
||||
projectVersion: '0.2.0',
|
||||
projectVersion: '1.5.1',
|
||||
jsonPassword: '',
|
||||
keyThreshold: 350,
|
||||
keyLockKey: 'F9',
|
||||
|
||||
+7
@@ -68,6 +68,12 @@ export interface AppInfo {
|
||||
}
|
||||
|
||||
export interface SmartAgriApi {
|
||||
clipboard: {
|
||||
writeText(text: string): Promise<void>
|
||||
}
|
||||
shell: {
|
||||
openExternal(url: string): Promise<void>
|
||||
}
|
||||
win: {
|
||||
minimize(): void
|
||||
toggleMaximize(): void
|
||||
@@ -119,6 +125,7 @@ export interface SmartAgriApi {
|
||||
}
|
||||
page: {
|
||||
snapshot(url: string): Promise<string | null>
|
||||
compress(png: Uint8Array): Promise<string | null>
|
||||
fetchMeta(url: string): Promise<{ title: string; desc: string } | null>
|
||||
}
|
||||
syncProject(pages: unknown[]): void
|
||||
|
||||
@@ -8,6 +8,12 @@ function on<T>(channel: string, cb: (payload: T) => void): () => void {
|
||||
}
|
||||
|
||||
const api = {
|
||||
clipboard: {
|
||||
writeText: (text: string): Promise<void> => ipcRenderer.invoke('clipboard:write', text)
|
||||
},
|
||||
shell: {
|
||||
openExternal: (url: string): Promise<void> => ipcRenderer.invoke('shell:openExternal', url)
|
||||
},
|
||||
win: {
|
||||
minimize: (): void => ipcRenderer.send('win:minimize'),
|
||||
toggleMaximize: (): void => ipcRenderer.send('win:toggle-maximize'),
|
||||
@@ -67,6 +73,7 @@ const api = {
|
||||
},
|
||||
page: {
|
||||
snapshot: (url: string): Promise<string | null> => ipcRenderer.invoke('page:snapshot', url),
|
||||
compress: (png: Uint8Array): Promise<string | null> => ipcRenderer.invoke('page:compress', png),
|
||||
fetchMeta: (url: string): Promise<{ title: string; desc: string } | null> =>
|
||||
ipcRenderer.invoke('page:fetchMeta', url)
|
||||
},
|
||||
|
||||
@@ -221,8 +221,12 @@ export default function Ribbon(props: {
|
||||
icon: m.icon,
|
||||
label: m.label,
|
||||
title: m.desc,
|
||||
type: props.configMenu === m.id ? ('accent' as const) : undefined,
|
||||
type: !m.href && props.configMenu === m.id ? ('accent' as const) : undefined,
|
||||
action: () => {
|
||||
if (m.href) {
|
||||
void window.api.shell.openExternal(m.href)
|
||||
return
|
||||
}
|
||||
setView('config')
|
||||
props.onConfigMenu(m.id)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
||||
import { useStore } from '../store'
|
||||
|
||||
export interface WebFrameHandle {
|
||||
reload: () => void
|
||||
@@ -31,8 +32,11 @@ const WebFrame = forwardRef<WebFrameHandle, WebFrameProps>(function WebFrame(
|
||||
wv.style.height = '100%'
|
||||
host.appendChild(wv)
|
||||
wvRef.current = wv
|
||||
// 注册当前预览 webview,供截图直接捕获预览区域内容
|
||||
useStore.getState().setPreviewWv(wv)
|
||||
wv.src = src
|
||||
return () => {
|
||||
useStore.getState().setPreviewWv(null)
|
||||
wv.remove()
|
||||
wvRef.current = null
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function AboutPanel(): React.JSX.Element {
|
||||
<div className="about-logo">🌾</div>
|
||||
<div className="about-title">{settings?.projectName ?? '智慧农业大数据可视化控制中心'}</div>
|
||||
<div className="about-ver">
|
||||
Version {settings?.projectVersion ?? '0.2.0'}
|
||||
Version {settings?.projectVersion ?? '1.5.1'}
|
||||
{info?.appVersion ? ` · 安装包 ${info.appVersion}` : ''}
|
||||
</div>
|
||||
<p className="about-desc">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useStore } from '../../store'
|
||||
import { toast } from '../../toast'
|
||||
import { nextMinor } from '../../utils'
|
||||
|
||||
export default function ProjectInfo(): React.JSX.Element {
|
||||
const settings = useStore((s) => s.settings)
|
||||
@@ -27,20 +28,27 @@ export default function ProjectInfo(): React.JSX.Element {
|
||||
}, [settings])
|
||||
|
||||
const save = async (): Promise<void> => {
|
||||
await st().updateSettings({ projectName: name.trim() || '智慧农业大数据可视化控制中心', projectVersion: ver.trim() || '0.2.0', jsonPassword: pwd })
|
||||
await st().updateSettings({ projectName: name.trim() || '智慧农业大数据可视化控制中心', projectVersion: ver.trim() || '1.5.1', jsonPassword: pwd })
|
||||
toast('项目信息与 JSON 密码已保存', 'ok')
|
||||
}
|
||||
|
||||
const copyPath = async (): Promise<void> => {
|
||||
if (!filePath) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(filePath)
|
||||
await window.api.clipboard.writeText(filePath)
|
||||
toast('工程路径已复制', 'ok')
|
||||
} catch {
|
||||
toast('复制失败,请手动选择复制', 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
const bumpMinor = async (): Promise<void> => {
|
||||
const nv = nextMinor(ver || '1.5.1')
|
||||
setVer(nv)
|
||||
await st().updateSettings({ projectVersion: nv })
|
||||
toast('次版本 +1 → ' + nv, 'ok')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cfg-section">
|
||||
<div className="cfg-card">
|
||||
@@ -52,7 +60,12 @@ export default function ProjectInfo(): React.JSX.Element {
|
||||
</div>
|
||||
<div className="cfg-field">
|
||||
<label>版本号</label>
|
||||
<input className="cfg-input" value={ver} onChange={(e) => setVer(e.target.value)} placeholder="例如:0.2.0" />
|
||||
<div className="cfg-ver">
|
||||
<input className="cfg-input" value={ver} onChange={(e) => setVer(e.target.value)} placeholder="例如:1.5.1" />
|
||||
<button className="btn" title="次版本编号 +1(1.5.1 → 0.3.0)" onClick={() => void bumpMinor()}>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="cfg-tip">项目名称与版本号会写入工程 JSON(appName / version),并作为「另存为」时的默认文件名。</p>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/** 配置页菜单项标识 */
|
||||
export type ConfigMenu = 'info' | 'remote' | 'shortcut' | 'publish' | 'device' | 'about'
|
||||
export type ConfigMenu = 'info' | 'remote' | 'shortcut' | 'publish' | 'device' | 'wenchuanyi' | 'about'
|
||||
|
||||
export interface ConfigMenuMeta {
|
||||
id: ConfigMenu
|
||||
icon: string
|
||||
label: string
|
||||
desc: string
|
||||
/** 外部链接:配置时点击用系统浏览器打开,不切换配置页 */
|
||||
href?: string
|
||||
}
|
||||
|
||||
/** 配置菜单定义(作为 Ribbon 上的按钮排列) */
|
||||
@@ -15,5 +17,6 @@ export const CONFIG_MENUS: ConfigMenuMeta[] = [
|
||||
{ id: 'shortcut', icon: '🖱️', label: '快捷方式', desc: '桌面快捷方式:打开 / 编辑 / 发布' },
|
||||
{ id: 'publish', icon: '🚀', label: '发布设置', desc: '快捷方式③的默认发布配置' },
|
||||
{ id: 'device', icon: '🔌', label: '设备测试', desc: '键盘 / 音箱 / 麦克风 / 屏幕测试' },
|
||||
{ id: 'wenchuanyi', icon: '📥', label: '文传易', desc: '用系统浏览器打开文传易网站', href: 'http://wenchuanyi.bbitcn.net/' },
|
||||
{ id: 'about', icon: 'ℹ️', label: '关于', desc: '版本、简介、运行环境与帮助' }
|
||||
]
|
||||
|
||||
+11
@@ -6,7 +6,18 @@ declare namespace Electron {
|
||||
reload(): void
|
||||
getURL(): string
|
||||
getWebContentsId(): number
|
||||
isLoading(): boolean
|
||||
capturePage(): Promise<Electron.NativeImage>
|
||||
setAttribute(name: string, value: string): void
|
||||
getAttribute(name: string): string | null
|
||||
}
|
||||
|
||||
interface NativeImage {
|
||||
isEmpty(): boolean
|
||||
toPNG(): Buffer
|
||||
toJPEG(quality: number): Buffer
|
||||
toDataURL(): string
|
||||
getSize(): { width: number; height: number }
|
||||
resize(options: { width?: number; height?: number; quality?: string }): NativeImage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import type { AppSettings, LoadPayload, Page, ProjectJson, ScreenInfo, ViewMode } from './types'
|
||||
import { defaultPages, jsonToPages, nextPageId, pageSrc, pagesToJson } from './utils'
|
||||
import { defaultPages, jsonToPages, nextPageId, nextPatch, pageSrc, pagesToJson } from './utils'
|
||||
import { toast } from './toast'
|
||||
import { askPassword } from './passwordState'
|
||||
|
||||
@@ -15,6 +15,8 @@ interface AppState {
|
||||
settings: AppSettings | null
|
||||
/** 工程文件操作进行中(打开/另存等),用于防抖防重入 */
|
||||
opening: boolean
|
||||
/** 编辑区预览 webview 引用(用于截图捕获实际预览内容) */
|
||||
previewWv: Electron.WebviewTag | null
|
||||
// actions
|
||||
setView: (v: ViewMode) => void
|
||||
selectPage: (i: number) => void
|
||||
@@ -25,6 +27,7 @@ interface AppState {
|
||||
movePage: (dir: number) => void
|
||||
delPage: () => void
|
||||
capturePreview: (index?: number) => Promise<void>
|
||||
setPreviewWv: (wv: Electron.WebviewTag | null) => void
|
||||
resetOrder: () => void
|
||||
setScreens: (s: ScreenInfo[]) => void
|
||||
setZoom: (z: number) => void
|
||||
@@ -78,6 +81,7 @@ export const useStore = create<AppState>((set, get) => {
|
||||
screens: [],
|
||||
zoom: 100,
|
||||
settings: null,
|
||||
previewWv: null,
|
||||
opening: false,
|
||||
|
||||
setView: (v) => set({ view: v }),
|
||||
@@ -157,7 +161,27 @@ export const useStore = create<AppState>((set, get) => {
|
||||
}
|
||||
toast('正在截图…', 'info')
|
||||
try {
|
||||
const data = await window.api.page.snapshot(src)
|
||||
// 优先截取编辑区预览 webview(所见即所得);非当前预览页时回退到按 URL 截图
|
||||
let data: string | null = null
|
||||
const wv = get().previewWv
|
||||
if (at === get().curIdx && wv) {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!wv.isLoading()) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const onStop = (): void => {
|
||||
wv.removeEventListener('did-stop-loading', onStop)
|
||||
resolve()
|
||||
}
|
||||
wv.addEventListener('did-stop-loading', onStop)
|
||||
setTimeout(resolve, 5000)
|
||||
})
|
||||
const img = await wv.capturePage()
|
||||
data = await window.api.page.compress(img.toPNG())
|
||||
} else {
|
||||
data = await window.api.page.snapshot(src)
|
||||
}
|
||||
if (data) {
|
||||
if (data === p.preview) {
|
||||
toast('页面内容无变化,缩略图已是最新', 'info')
|
||||
@@ -186,6 +210,7 @@ export const useStore = create<AppState>((set, get) => {
|
||||
},
|
||||
setScreens: (s) => set({ screens: s }),
|
||||
setZoom: (z) => set({ zoom: Math.min(160, Math.max(50, z)) }),
|
||||
setPreviewWv: (wv) => set({ previewWv: wv }),
|
||||
|
||||
loadSettings: async () => {
|
||||
const s = await window.api.settings.get()
|
||||
@@ -218,21 +243,25 @@ export const useStore = create<AppState>((set, get) => {
|
||||
|
||||
save: async () => {
|
||||
const { filePath, pages, settings } = get()
|
||||
const meta = { appName: settings?.projectName, version: settings?.projectVersion }
|
||||
// 每保存一次,修订号 +1
|
||||
const newVer = nextPatch(settings?.projectVersion || '1.5.1')
|
||||
const meta = { appName: settings?.projectName, version: newVer }
|
||||
const data = JSON.stringify(pagesToJson(pages, meta), null, 2)
|
||||
const pwd = settings?.jsonPassword || undefined
|
||||
try {
|
||||
if (filePath) {
|
||||
const r = await window.api.project.save(filePath, data, pwd)
|
||||
if (r) {
|
||||
await get().updateSettings({ projectVersion: newVer })
|
||||
set({ dirty: false })
|
||||
toast('工程已保存', 'ok')
|
||||
toast('工程已保存 · 修订号 v' + newVer, 'ok')
|
||||
}
|
||||
} else {
|
||||
const path = await window.api.project.saveAs(data, pwd)
|
||||
if (path) {
|
||||
await get().updateSettings({ projectVersion: newVer })
|
||||
set({ filePath: path, dirty: false })
|
||||
toast('工程已保存为 ' + path.split(/[\\/]/).pop(), 'ok')
|
||||
toast('工程已保存为 ' + path.split(/[\\/]/).pop() + ' · 修订号 v' + newVer, 'ok')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1293,6 +1293,15 @@ select {
|
||||
.cfg-path .cfg-row-value {
|
||||
max-width: 70%;
|
||||
}
|
||||
.cfg-ver {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.cfg-ver .cfg-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cfg-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -46,7 +46,7 @@ export function pageSrc(p: Page): string {
|
||||
export function pagesToJson(pages: Page[], meta?: { appName?: string; version?: string }): ProjectJson {
|
||||
return {
|
||||
appName: meta?.appName || '智慧农业大数据可视化控制中心',
|
||||
version: meta?.version || '0.2.0',
|
||||
version: meta?.version || '1.5.1',
|
||||
pages: pages.map((p) => ({
|
||||
PageID: p.id,
|
||||
PageTitle: p.title,
|
||||
@@ -77,6 +77,33 @@ export function jsonToPages(json: ProjectJson): Page[] {
|
||||
}))
|
||||
}
|
||||
|
||||
/** 版本号:大版本 +1(1.5.1 → 1.2.0),非数字开头原样返回 */
|
||||
export function nextMajor(v: string): string {
|
||||
const m = v.trim().match(/^(\d+)(.*)$/)
|
||||
if (!m) return v.trim()
|
||||
return String(Number(m[1]) + 1) + m[2]
|
||||
}
|
||||
|
||||
/** 版本号:次版本 +1(1.5.1 → 0.3.0,1 → 1.1.0),非数字开头回退 '1.5.1' */
|
||||
export function nextMinor(v: string): string {
|
||||
const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||
if (!m) return '1.5.1'
|
||||
const major = m[1]
|
||||
const minor = m[2] ?? '0'
|
||||
const patch = m[3] ?? '0'
|
||||
return `${major}.${Number(minor) + 1}.${patch}`
|
||||
}
|
||||
|
||||
/** 版本号:修订号 +1(1.5.1 → 0.2.1,0.2 → 0.2.1,1 → 1.0.1),非数字开头回退 '1.5.1' */
|
||||
export function nextPatch(v: string): string {
|
||||
const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||
if (!m) return '1.5.1'
|
||||
const major = m[1]
|
||||
const minor = m[2] ?? '0'
|
||||
const patch = m[3] ?? '0'
|
||||
return `${major}.${minor}.${Number(patch) + 1}`
|
||||
}
|
||||
|
||||
export function defaultPages(): Page[] {
|
||||
return [
|
||||
{ id: 'P-001', title: '农业数据总览', desc: '全基地农业数据总览', screen: 1, ctrl: 1, display: true, url: 'sample-screen.html', params: 'no=1\ntitle=农业数据总览' },
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function Canvas(): React.JSX.Element {
|
||||
const copyUrl = async (): Promise<void> => {
|
||||
if (!baseUrl) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(baseUrl)
|
||||
await window.api.clipboard.writeText(baseUrl)
|
||||
toast('地址已复制', 'ok')
|
||||
} catch {
|
||||
toast('复制失败,请手动选择复制', 'warn')
|
||||
|
||||
Reference in New Issue
Block a user