v1.5.4: 椤圭洰淇℃伅鍏ㄩ噺淇濆瓨淇+鍏抽棴鍊掕鏃跺脊绐?鍚姩鎭㈠涓婃宸ョ▼+绌虹櫧榛樿
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "smart-agri-center",
|
"name": "smart-agri-center",
|
||||||
"version": "1.5.1",
|
"version": "1.5.4",
|
||||||
"description": "智慧农业大数据可视化控制中心(Electron + React + TypeScript)",
|
"description": "智慧农业大数据可视化控制中心(Electron + React + TypeScript)",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "FanHongCai",
|
"author": "FanHongCai",
|
||||||
|
|||||||
@@ -129,10 +129,10 @@ export function registerIpc(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ===== 工程文件(带 JSON 读写密码)=====
|
// ===== 工程文件(带 JSON 读写密码)=====
|
||||||
/** 记住用户最后一次选择/保存工程的文件夹 */
|
/** 记住用户最后一次选择/保存的工程(文件夹 + 文件完整路径,供启动时自动恢复) */
|
||||||
const rememberDir = async (p: string): Promise<void> => {
|
const rememberProject = async (p: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await saveSettings({ lastDir: dirname(p) })
|
await saveSettings({ lastDir: dirname(p), lastProjectPath: p })
|
||||||
} catch {
|
} catch {
|
||||||
// 记录失败不影响主流程
|
// 记录失败不影响主流程
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ export function registerIpc(): void {
|
|||||||
})
|
})
|
||||||
if (r.canceled || !r.filePath) return null
|
if (r.canceled || !r.filePath) return null
|
||||||
const res = await writeProjectFile(r.filePath, payload.data, pwd)
|
const res = await writeProjectFile(r.filePath, payload.data, pwd)
|
||||||
if (res.ok) await rememberDir(r.filePath)
|
if (res.ok) await rememberProject(r.filePath)
|
||||||
return res.ok ? r.filePath : null
|
return res.ok ? r.filePath : null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -160,6 +160,7 @@ export function registerIpc(): void {
|
|||||||
const settings = await loadSettings()
|
const settings = await loadSettings()
|
||||||
const pwd = payload.password ?? settings.jsonPassword
|
const pwd = payload.password ?? settings.jsonPassword
|
||||||
const res = await writeProjectFile(payload.path, payload.data, pwd)
|
const res = await writeProjectFile(payload.path, payload.data, pwd)
|
||||||
|
if (res.ok) await rememberProject(payload.path)
|
||||||
return res.ok ? payload.path : null
|
return res.ok ? payload.path : null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -175,14 +176,19 @@ export function registerIpc(): void {
|
|||||||
if (r.canceled || !r.filePaths[0]) return null
|
if (r.canceled || !r.filePaths[0]) return null
|
||||||
const pwd = password ?? settings.jsonPassword
|
const pwd = password ?? settings.jsonPassword
|
||||||
const res = await readProjectFile(r.filePaths[0], pwd)
|
const res = await readProjectFile(r.filePaths[0], pwd)
|
||||||
await rememberDir(r.filePaths[0])
|
await rememberProject(r.filePaths[0])
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
|
|
||||||
// 打开指定路径工程(用于密码输入重试 / 快捷方式场景)
|
// 打开指定路径工程(用于密码输入重试 / 快捷方式场景 / 启动自动恢复)
|
||||||
ipcMain.handle('project:openAt', (_e, payload: { path: string; password?: string }) =>
|
ipcMain.handle('project:openAt', async (_e, payload: { path: string; password?: string }) => {
|
||||||
readProjectFile(payload.path, payload.password ?? '')
|
const res = await readProjectFile(payload.path, payload.password ?? '')
|
||||||
)
|
await rememberProject(payload.path)
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回上次编辑的工程文件路径(启动时自动恢复用),无则返回 null
|
||||||
|
ipcMain.handle('project:lastPath', async () => (await loadSettings()).lastProjectPath || null)
|
||||||
|
|
||||||
// ===== 应用设置 =====
|
// ===== 应用设置 =====
|
||||||
ipcMain.handle('settings:get', () => loadSettings())
|
ipcMain.handle('settings:get', () => loadSettings())
|
||||||
@@ -328,32 +334,41 @@ export function registerIpc(): void {
|
|||||||
|
|
||||||
let dirty = false
|
let dirty = false
|
||||||
let forceQuit = false
|
let forceQuit = false
|
||||||
|
/** 关闭倒计时秒数:到点后自动退出主程序 */
|
||||||
|
const QUIT_COUNTDOWN = 10
|
||||||
|
let quitGuardTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
function setDirtyFlag(d: boolean): void {
|
function setDirtyFlag(d: boolean): void {
|
||||||
dirty = d
|
dirty = d
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installCloseGuard(win: BrowserWindow): void {
|
export function installCloseGuard(win: BrowserWindow): void {
|
||||||
|
// 渲染层确认退出 / 倒计时结束 → 真正退出
|
||||||
|
ipcMain.removeAllListeners('app:quitForce')
|
||||||
|
ipcMain.on('app:quitForce', () => {
|
||||||
|
forceQuit = true
|
||||||
|
if (quitGuardTimer) {
|
||||||
|
clearTimeout(quitGuardTimer)
|
||||||
|
quitGuardTimer = null
|
||||||
|
}
|
||||||
|
closeAllScreens()
|
||||||
|
if (!win.isDestroyed()) win.close()
|
||||||
|
})
|
||||||
|
|
||||||
win.on('close', (e) => {
|
win.on('close', (e) => {
|
||||||
if (forceQuit) return
|
if (forceQuit) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const warn = dirty ? '当前工程有未保存的修改。\n\n' : ''
|
// 通知渲染层弹出倒计时确认框(由渲染层决定是否退出)
|
||||||
void dialog
|
if (!win.isDestroyed()) win.webContents.send('app:requestQuit')
|
||||||
.showMessageBox(win, {
|
// 兜底:若渲染层在倒计时结束后仍未响应,则强制退出,避免卡死
|
||||||
type: 'warning',
|
if (quitGuardTimer) clearTimeout(quitGuardTimer)
|
||||||
title: '确认退出',
|
quitGuardTimer = setTimeout(
|
||||||
message: `${warn}确定要退出智慧农业大数据可视化控制中心吗?`,
|
() => {
|
||||||
buttons: ['退出', '取消'],
|
forceQuit = true
|
||||||
defaultId: 1,
|
closeAllScreens()
|
||||||
cancelId: 1
|
if (!win.isDestroyed()) win.close()
|
||||||
})
|
},
|
||||||
.then((r) => {
|
(QUIT_COUNTDOWN + 5) * 1000
|
||||||
if (r.response === 0) {
|
)
|
||||||
forceQuit = true
|
|
||||||
// 先关闭所有播放中的子页面(屏幕窗体/轮播/探测/主屏播放),再关闭主窗口
|
|
||||||
closeAllScreens()
|
|
||||||
win.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,19 @@ export interface AppSettings {
|
|||||||
publish: PublishDefaults
|
publish: PublishDefaults
|
||||||
/** 上次打开/另存为工程的文件夹,用于记住上次位置 */
|
/** 上次打开/另存为工程的文件夹,用于记住上次位置 */
|
||||||
lastDir: string
|
lastDir: string
|
||||||
|
/** 上次打开/保存/另存为的工程文件完整路径,用于启动时自动恢复 */
|
||||||
|
lastProjectPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULTS: AppSettings = {
|
const DEFAULTS: AppSettings = {
|
||||||
projectName: '智慧农业大数据可视化控制中心',
|
projectName: '智慧农业大数据可视化控制中心',
|
||||||
projectVersion: '1.5.1',
|
projectVersion: '1.5.4',
|
||||||
jsonPassword: '',
|
jsonPassword: '',
|
||||||
keyThreshold: 350,
|
keyThreshold: 350,
|
||||||
keyLockKey: 'F9',
|
keyLockKey: 'F9',
|
||||||
publish: { mode: 'multi', screenIndex: 1, alsoMain: false, intervalSec: 10 },
|
publish: { mode: 'multi', screenIndex: 1, alsoMain: false, intervalSec: 10 },
|
||||||
lastDir: ''
|
lastDir: '',
|
||||||
|
lastProjectPath: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache: AppSettings | null = null
|
let cache: AppSettings | null = null
|
||||||
|
|||||||
+6
@@ -107,6 +107,8 @@ export interface SmartAgriApi {
|
|||||||
save(path: string, data: string, password?: string): Promise<string | null>
|
save(path: string, data: string, password?: string): Promise<string | null>
|
||||||
open(password?: string): Promise<{ path: string; json: ProjectJson | null; error?: string } | null>
|
open(password?: string): Promise<{ path: string; json: ProjectJson | null; error?: string } | null>
|
||||||
openAt(path: string, password?: string): Promise<{ path: string; json: ProjectJson | null; error?: string }>
|
openAt(path: string, password?: string): Promise<{ path: string; json: ProjectJson | null; error?: string }>
|
||||||
|
/** 返回上次编辑的工程文件路径(启动自动恢复用),无则返回 null */
|
||||||
|
lastPath(): Promise<string | null>
|
||||||
}
|
}
|
||||||
settings: {
|
settings: {
|
||||||
get(): Promise<AppSettings>
|
get(): Promise<AppSettings>
|
||||||
@@ -119,6 +121,10 @@ export interface SmartAgriApi {
|
|||||||
info(): Promise<AppInfo>
|
info(): Promise<AppInfo>
|
||||||
consumeLoad(): Promise<LoadPayload | null>
|
consumeLoad(): Promise<LoadPayload | null>
|
||||||
onLoadProject(cb: (payload: LoadPayload) => void): () => void
|
onLoadProject(cb: (payload: LoadPayload) => void): () => void
|
||||||
|
/** 主进程拦截关闭时触发,渲染层应弹出倒计时确认框 */
|
||||||
|
onRequestQuit(cb: () => void): () => void
|
||||||
|
/** 倒计时结束或用户确认后,通知主进程强制退出 */
|
||||||
|
quitForce(): void
|
||||||
}
|
}
|
||||||
key: {
|
key: {
|
||||||
onScreenKey(cb: (n: number) => void): () => void
|
onScreenKey(cb: (n: number) => void): () => void
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ const api = {
|
|||||||
open: (password?: string): Promise<{ path: string; json: unknown | null; error?: string } | null> =>
|
open: (password?: string): Promise<{ path: string; json: unknown | null; error?: string } | null> =>
|
||||||
ipcRenderer.invoke('project:open', password),
|
ipcRenderer.invoke('project:open', password),
|
||||||
openAt: (path: string, password?: string): Promise<{ path: string; json: unknown | null; error?: string }> =>
|
openAt: (path: string, password?: string): Promise<{ path: string; json: unknown | null; error?: string }> =>
|
||||||
ipcRenderer.invoke('project:openAt', { path, password })
|
ipcRenderer.invoke('project:openAt', { path, password }),
|
||||||
|
lastPath: (): Promise<string | null> => ipcRenderer.invoke('project:lastPath')
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
get: (): Promise<unknown> => ipcRenderer.invoke('settings:get'),
|
get: (): Promise<unknown> => ipcRenderer.invoke('settings:get'),
|
||||||
@@ -66,7 +67,9 @@ const api = {
|
|||||||
app: {
|
app: {
|
||||||
info: (): Promise<unknown> => ipcRenderer.invoke('app:info'),
|
info: (): Promise<unknown> => ipcRenderer.invoke('app:info'),
|
||||||
consumeLoad: (): Promise<unknown> => ipcRenderer.invoke('app:consumeLoad'),
|
consumeLoad: (): Promise<unknown> => ipcRenderer.invoke('app:consumeLoad'),
|
||||||
onLoadProject: (cb: (payload: unknown) => void): (() => void) => on('app:loadProject', cb)
|
onLoadProject: (cb: (payload: unknown) => void): (() => void) => on('app:loadProject', cb),
|
||||||
|
onRequestQuit: (cb: () => void): (() => void) => on('app:requestQuit', cb),
|
||||||
|
quitForce: (): void => ipcRenderer.send('app:quitForce')
|
||||||
},
|
},
|
||||||
key: {
|
key: {
|
||||||
onScreenKey: (cb: (n: number) => void): (() => void) => on('key:switch', cb)
|
onScreenKey: (cb: (n: number) => void): (() => void) => on('key:switch', cb)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Ribbon from './components/Ribbon'
|
|||||||
import StatusBar from './components/StatusBar'
|
import StatusBar from './components/StatusBar'
|
||||||
import Toast from './components/Toast'
|
import Toast from './components/Toast'
|
||||||
import { PasswordModal } from './components/PasswordModal'
|
import { PasswordModal } from './components/PasswordModal'
|
||||||
|
import { QuitConfirmModal } from './components/QuitConfirmModal'
|
||||||
import EditView from './views/EditView'
|
import EditView from './views/EditView'
|
||||||
import ConfigView from './views/ConfigView'
|
import ConfigView from './views/ConfigView'
|
||||||
import { useStore } from './store'
|
import { useStore } from './store'
|
||||||
@@ -16,6 +17,7 @@ export default function App(): React.JSX.Element {
|
|||||||
const view = useStore((s) => s.view)
|
const view = useStore((s) => s.view)
|
||||||
const pages = useStore((s) => s.pages)
|
const pages = useStore((s) => s.pages)
|
||||||
const [configMenu, setConfigMenu] = useState<ConfigMenu>('info')
|
const [configMenu, setConfigMenu] = useState<ConfigMenu>('info')
|
||||||
|
const [quitOpen, setQuitOpen] = useState(false)
|
||||||
|
|
||||||
// 监听屏幕状态变化
|
// 监听屏幕状态变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,21 +42,35 @@ export default function App(): React.JSX.Element {
|
|||||||
)
|
)
|
||||||
}, [pages])
|
}, [pages])
|
||||||
|
|
||||||
// 启动:加载应用设置 + 消费/监听快捷方式打开的项目
|
// 启动:加载应用设置 + 消费/监听快捷方式打开的项目;无快捷方式工程时自动恢复上次编辑的工程
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const st = useStore.getState
|
const st = useStore.getState
|
||||||
void st().loadSettings()
|
|
||||||
const handle = (p: unknown): void => {
|
const handle = (p: unknown): void => {
|
||||||
const lp = p as LoadPayload | null
|
const lp = p as LoadPayload | null
|
||||||
if (!lp?.json) return
|
if (!lp?.json) return
|
||||||
const mode = st().loadProject(lp)
|
const mode = st().loadProject(lp)
|
||||||
if (mode === 'publish') void applyPublishDefaults()
|
if (mode === 'publish') void applyPublishDefaults()
|
||||||
}
|
}
|
||||||
void window.api.app.consumeLoad().then(handle)
|
void (async () => {
|
||||||
|
await st().loadSettings()
|
||||||
|
const lp = (await window.api.app.consumeLoad()) as LoadPayload | null
|
||||||
|
if (lp?.json) {
|
||||||
|
handle(lp)
|
||||||
|
} else {
|
||||||
|
// 无快捷方式指定工程时,默认加载最后一次编辑的工程
|
||||||
|
await st().loadLastProject()
|
||||||
|
}
|
||||||
|
})()
|
||||||
const off = window.api.app.onLoadProject(handle)
|
const off = window.api.app.onLoadProject(handle)
|
||||||
return off
|
return off
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 主进程拦截关闭 → 弹出倒计时 / 保存确认框
|
||||||
|
useEffect(() => {
|
||||||
|
const off = window.api.app.onRequestQuit(() => setQuitOpen(true))
|
||||||
|
return off
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 主窗口数字键切换页面
|
// 主窗口数字键切换页面
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const h = (e: KeyboardEvent): void => {
|
const h = (e: KeyboardEvent): void => {
|
||||||
@@ -86,6 +102,31 @@ export default function App(): React.JSX.Element {
|
|||||||
<StatusBar />
|
<StatusBar />
|
||||||
<Toast />
|
<Toast />
|
||||||
<PasswordModal />
|
<PasswordModal />
|
||||||
|
{quitOpen && (
|
||||||
|
<QuitConfirmModal
|
||||||
|
seconds={10}
|
||||||
|
hasUnsaved={
|
||||||
|
useStore.getState().dirty ||
|
||||||
|
((): boolean => {
|
||||||
|
const s = useStore.getState()
|
||||||
|
return !!s.settings &&
|
||||||
|
(s.settings.projectName !== s.savedInfo.projectName ||
|
||||||
|
s.settings.projectVersion !== s.savedInfo.projectVersion ||
|
||||||
|
s.settings.jsonPassword !== s.savedInfo.jsonPassword)
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
onSaveQuit={async () => {
|
||||||
|
setQuitOpen(false)
|
||||||
|
await useStore.getState().save()
|
||||||
|
window.api.app.quitForce()
|
||||||
|
}}
|
||||||
|
onQuit={() => {
|
||||||
|
setQuitOpen(false)
|
||||||
|
window.api.app.quitForce()
|
||||||
|
}}
|
||||||
|
onCancel={() => setQuitOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 倒计时秒数,无未保存变更到点自动退出 */
|
||||||
|
seconds: number
|
||||||
|
/** 是否存在未保存的修改(页面或项目信息),有则改为「是否保存」确认而非倒计时 */
|
||||||
|
hasUnsaved: boolean
|
||||||
|
/** 保存并退出(仅 hasUnsaved 时可用) */
|
||||||
|
onSaveQuit: () => void
|
||||||
|
/** 直接退出(放弃保存) */
|
||||||
|
onQuit: () => void
|
||||||
|
/** 取消退出 */
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭主程序前的确认弹窗:
|
||||||
|
* - 无未保存变更:倒计时归零自动退出
|
||||||
|
* - 有未保存变更:提示是否保存后再退出 */
|
||||||
|
export function QuitConfirmModal({ seconds, hasUnsaved, onSaveQuit, onQuit, onCancel }: Props): React.JSX.Element {
|
||||||
|
const [left, setLeft] = useState(seconds)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasUnsaved) return // 有变更时不自动倒计时退出,需用户决策
|
||||||
|
if (left <= 0) {
|
||||||
|
onQuit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const t = setTimeout(() => setLeft((n) => n - 1), 1000)
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}, [hasUnsaved, left, onQuit])
|
||||||
|
|
||||||
|
const pct = Math.max(0, Math.min(100, (left / seconds) * 100))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" role="dialog" aria-modal="true">
|
||||||
|
<div className="modal quit-modal">
|
||||||
|
<div className="modal-head">
|
||||||
|
<span className="modal-title">{hasUnsaved ? '保存更改' : '正在退出'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
{hasUnsaved ? (
|
||||||
|
<p className="m-hint">项目有未保存的修改,是否先保存工程后再退出?</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="m-hint">
|
||||||
|
客户端将在 <b className="quit-count">{left}</b> 秒后自动关闭。
|
||||||
|
</p>
|
||||||
|
<div className="quit-progress">
|
||||||
|
<div className="quit-progress-bar" style={{ width: `${pct}%` }} />
|
||||||
|
</div>
|
||||||
|
<p className="m-hint">如有未保存的修改,请先取消并保存工程。</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="modal-foot">
|
||||||
|
{hasUnsaved ? (
|
||||||
|
<>
|
||||||
|
<button className="btn" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={onQuit}>
|
||||||
|
不保存退出
|
||||||
|
</button>
|
||||||
|
<button className="btn primary" onClick={onSaveQuit}>
|
||||||
|
保存并退出
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button className="btn" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button className="btn primary" onClick={onQuit}>
|
||||||
|
立即退出
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export default function AboutPanel(): React.JSX.Element {
|
|||||||
<div className="about-logo">🌾</div>
|
<div className="about-logo">🌾</div>
|
||||||
<div className="about-title">{settings?.projectName ?? '智慧农业大数据可视化控制中心'}</div>
|
<div className="about-title">{settings?.projectName ?? '智慧农业大数据可视化控制中心'}</div>
|
||||||
<div className="about-ver">
|
<div className="about-ver">
|
||||||
Version {settings?.projectVersion ?? '1.5.1'}
|
Version {settings?.projectVersion ?? '1.5.4'}
|
||||||
{info?.appVersion ? ` · 安装包 ${info.appVersion}` : ''}
|
{info?.appVersion ? ` · 安装包 ${info.appVersion}` : ''}
|
||||||
</div>
|
</div>
|
||||||
<p className="about-desc">
|
<p className="about-desc">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useStore } from '../../store'
|
import { useStore } from '../../store'
|
||||||
import { toast } from '../../toast'
|
import { toast } from '../../toast'
|
||||||
import { nextMinor } from '../../utils'
|
import { nextMinor } from '../../utils'
|
||||||
@@ -8,7 +8,13 @@ export default function ProjectInfo(): React.JSX.Element {
|
|||||||
const pages = useStore((s) => s.pages)
|
const pages = useStore((s) => s.pages)
|
||||||
const screens = useStore((s) => s.screens)
|
const screens = useStore((s) => s.screens)
|
||||||
const filePath = useStore((s) => s.filePath)
|
const filePath = useStore((s) => s.filePath)
|
||||||
const opening = useStore((s) => s.opening)
|
const infoDirty = useStore((s) =>
|
||||||
|
s.settings
|
||||||
|
? s.settings.projectName !== s.savedInfo.projectName ||
|
||||||
|
s.settings.projectVersion !== s.savedInfo.projectVersion ||
|
||||||
|
s.settings.jsonPassword !== s.savedInfo.jsonPassword
|
||||||
|
: false
|
||||||
|
)
|
||||||
const st = useStore.getState
|
const st = useStore.getState
|
||||||
|
|
||||||
const [name, setName] = useState(settings?.projectName ?? '')
|
const [name, setName] = useState(settings?.projectName ?? '')
|
||||||
@@ -16,20 +22,26 @@ export default function ProjectInfo(): React.JSX.Element {
|
|||||||
const [pwd, setPwd] = useState(settings?.jsonPassword ?? '')
|
const [pwd, setPwd] = useState(settings?.jsonPassword ?? '')
|
||||||
const [showPwd, setShowPwd] = useState(false)
|
const [showPwd, setShowPwd] = useState(false)
|
||||||
|
|
||||||
// 设置异步加载完成后同步一次
|
// 跟随设置变化同步字段(新建/打开工程初始化项目信息时立即刷新)
|
||||||
const synced = useRef(false)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings && !synced.current) {
|
if (!settings) return
|
||||||
synced.current = true
|
setName(settings.projectName)
|
||||||
setName(settings.projectName)
|
setVer(settings.projectVersion)
|
||||||
setVer(settings.projectVersion)
|
setPwd(settings.jsonPassword)
|
||||||
setPwd(settings.jsonPassword)
|
|
||||||
}
|
|
||||||
}, [settings])
|
}, [settings])
|
||||||
|
|
||||||
const save = async (): Promise<void> => {
|
// 项目信息实时同步到设置(关闭系统时会提示是否保存)
|
||||||
await st().updateSettings({ projectName: name.trim() || '智慧农业大数据可视化控制中心', projectVersion: ver.trim() || '1.5.1', jsonPassword: pwd })
|
const syncName = (v: string): void => {
|
||||||
toast('项目信息与 JSON 密码已保存', 'ok')
|
setName(v)
|
||||||
|
void st().updateSettings({ projectName: v.trim() || '新建大数据可视化工程' })
|
||||||
|
}
|
||||||
|
const syncVer = (v: string): void => {
|
||||||
|
setVer(v)
|
||||||
|
void st().updateSettings({ projectVersion: v.trim() || '1.0.0' })
|
||||||
|
}
|
||||||
|
const syncPwd = (v: string): void => {
|
||||||
|
setPwd(v)
|
||||||
|
void st().updateSettings({ jsonPassword: v })
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyPath = async (): Promise<void> => {
|
const copyPath = async (): Promise<void> => {
|
||||||
@@ -43,7 +55,7 @@ export default function ProjectInfo(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bumpMinor = async (): Promise<void> => {
|
const bumpMinor = async (): Promise<void> => {
|
||||||
const nv = nextMinor(ver || '1.5.1')
|
const nv = nextMinor(ver || '1.0.0')
|
||||||
setVer(nv)
|
setVer(nv)
|
||||||
await st().updateSettings({ projectVersion: nv })
|
await st().updateSettings({ projectVersion: nv })
|
||||||
toast('次版本 +1 → ' + nv, 'ok')
|
toast('次版本 +1 → ' + nv, 'ok')
|
||||||
@@ -56,12 +68,12 @@ export default function ProjectInfo(): React.JSX.Element {
|
|||||||
<div className="cfg-card-body">
|
<div className="cfg-card-body">
|
||||||
<div className="cfg-field">
|
<div className="cfg-field">
|
||||||
<label>项目名称</label>
|
<label>项目名称</label>
|
||||||
<input className="cfg-input" value={name} onChange={(e) => setName(e.target.value)} placeholder="例如:智慧农业大数据可视化控制中心" />
|
<input className="cfg-input" value={name} onChange={(e) => syncName(e.target.value)} placeholder="例如:智慧农业大数据可视化控制中心" />
|
||||||
</div>
|
</div>
|
||||||
<div className="cfg-field">
|
<div className="cfg-field">
|
||||||
<label>版本号</label>
|
<label>版本号</label>
|
||||||
<div className="cfg-ver">
|
<div className="cfg-ver">
|
||||||
<input className="cfg-input" value={ver} onChange={(e) => setVer(e.target.value)} placeholder="例如:1.5.1" />
|
<input className="cfg-input" value={ver} onChange={(e) => syncVer(e.target.value)} placeholder="例如:1.0.0" />
|
||||||
<button className="btn" title="次版本编号 +1(1.5.1 → 0.3.0)" onClick={() => void bumpMinor()}>
|
<button className="btn" title="次版本编号 +1(1.5.1 → 0.3.0)" onClick={() => void bumpMinor()}>
|
||||||
+1
|
+1
|
||||||
</button>
|
</button>
|
||||||
@@ -84,7 +96,7 @@ export default function ProjectInfo(): React.JSX.Element {
|
|||||||
className="cfg-input"
|
className="cfg-input"
|
||||||
type={showPwd ? 'text' : 'password'}
|
type={showPwd ? 'text' : 'password'}
|
||||||
value={pwd}
|
value={pwd}
|
||||||
onChange={(e) => setPwd(e.target.value)}
|
onChange={(e) => syncPwd(e.target.value)}
|
||||||
placeholder="留空则不加密"
|
placeholder="留空则不加密"
|
||||||
/>
|
/>
|
||||||
<button className="btn" onClick={() => setShowPwd(!showPwd)}>
|
<button className="btn" onClick={() => setShowPwd(!showPwd)}>
|
||||||
@@ -121,20 +133,7 @@ export default function ProjectInfo(): React.JSX.Element {
|
|||||||
<span className="cfg-row-label">屏幕数量</span>
|
<span className="cfg-row-label">屏幕数量</span>
|
||||||
<span className="cfg-row-value">{screens.length > 0 ? screens.length : '未检测'}</span>
|
<span className="cfg-row-value">{screens.length > 0 ? screens.length : '未检测'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pub-actions">
|
{infoDirty && <p className="cfg-tip info-dirty">项目信息已修改,关闭系统时将提示是否保存。</p>}
|
||||||
<button className="btn primary" onClick={() => void save()}>
|
|
||||||
💾 保存配置
|
|
||||||
</button>
|
|
||||||
<button className="btn" disabled={opening} onClick={() => void st().open()}>
|
|
||||||
{opening ? '⏳ 打开中…' : '📂 打开工程'}
|
|
||||||
</button>
|
|
||||||
<button className="btn" onClick={() => void st().save()}>
|
|
||||||
💾 保存工程
|
|
||||||
</button>
|
|
||||||
<button className="btn" onClick={() => void st().saveAs()}>
|
|
||||||
📤 另存为
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface AppState {
|
|||||||
screens: ScreenInfo[]
|
screens: ScreenInfo[]
|
||||||
zoom: number
|
zoom: number
|
||||||
settings: AppSettings | null
|
settings: AppSettings | null
|
||||||
|
/** 上次「保存/打开/新建工程」时的项目信息快照,用于派生项目信息是否已变更 */
|
||||||
|
savedInfo: { projectName: string; projectVersion: string; jsonPassword: string }
|
||||||
/** 工程文件操作进行中(打开/另存等),用于防抖防重入 */
|
/** 工程文件操作进行中(打开/另存等),用于防抖防重入 */
|
||||||
opening: boolean
|
opening: boolean
|
||||||
/** 编辑区预览 webview 引用(用于截图捕获实际预览内容) */
|
/** 编辑区预览 webview 引用(用于截图捕获实际预览内容) */
|
||||||
@@ -33,6 +35,8 @@ interface AppState {
|
|||||||
setZoom: (z: number) => void
|
setZoom: (z: number) => void
|
||||||
loadSettings: () => Promise<AppSettings>
|
loadSettings: () => Promise<AppSettings>
|
||||||
updateSettings: (patch: Partial<AppSettings>) => Promise<AppSettings>
|
updateSettings: (patch: Partial<AppSettings>) => Promise<AppSettings>
|
||||||
|
/** 启动时自动恢复上次编辑的工程(无则保留示例工程) */
|
||||||
|
loadLastProject: () => Promise<void>
|
||||||
newProject: () => void
|
newProject: () => void
|
||||||
loadProject: (payload: LoadPayload) => 'edit' | 'publish'
|
loadProject: (payload: LoadPayload) => 'edit' | 'publish'
|
||||||
save: () => Promise<void>
|
save: () => Promise<void>
|
||||||
@@ -57,10 +61,25 @@ function blankPage(pages: Page[], id: string, title: string): Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用工程 JSON 到编辑状态 */
|
/** 应用工程 JSON 到编辑状态(页面 + 项目信息随工程一起载入配置页) */
|
||||||
function applyProject(get: () => AppState, set: (p: Partial<AppState>) => void, path: string, json: ProjectJson): void {
|
async function applyProject(get: () => AppState, set: (p: Partial<AppState>) => void, path: string, json: ProjectJson): Promise<void> {
|
||||||
const pages = jsonToPages(json)
|
const pages = jsonToPages(json)
|
||||||
set({ pages, curIdx: 0, filePath: path, dirty: false })
|
// 项目信息从工程文件读取,写入 settings,使配置页显示该项目的信息
|
||||||
|
const cur = get().settings
|
||||||
|
const projectName = json.projectInfo?.projectName ?? json.appName ?? cur?.projectName ?? '新建大数据可视化工程'
|
||||||
|
const projectVersion = json.projectInfo?.projectVersion ?? json.version ?? cur?.projectVersion ?? '1.0.0'
|
||||||
|
let nextSettings = cur
|
||||||
|
if (cur && (cur.projectName !== projectName || cur.projectVersion !== projectVersion)) {
|
||||||
|
nextSettings = await get().updateSettings({ projectName, projectVersion })
|
||||||
|
}
|
||||||
|
set({
|
||||||
|
pages,
|
||||||
|
curIdx: 0,
|
||||||
|
filePath: path,
|
||||||
|
dirty: false,
|
||||||
|
settings: nextSettings,
|
||||||
|
savedInfo: { projectName, projectVersion, jsonPassword: nextSettings?.jsonPassword ?? cur?.jsonPassword ?? '' }
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
window.api.syncProject(pages.map((p) => ({ ctrl: p.ctrl, title: p.title, url: pageSrc(p) })))
|
window.api.syncProject(pages.map((p) => ({ ctrl: p.ctrl, title: p.title, url: pageSrc(p) })))
|
||||||
} catch {
|
} catch {
|
||||||
@@ -81,6 +100,7 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
screens: [],
|
screens: [],
|
||||||
zoom: 100,
|
zoom: 100,
|
||||||
settings: null,
|
settings: null,
|
||||||
|
savedInfo: { projectName: '', projectVersion: '', jsonPassword: '' },
|
||||||
previewWv: null,
|
previewWv: null,
|
||||||
opening: false,
|
opening: false,
|
||||||
|
|
||||||
@@ -234,18 +254,33 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
params: ''
|
params: ''
|
||||||
}
|
}
|
||||||
set({ pages: [blank], curIdx: 0, filePath: null, dirty: true })
|
set({ pages: [blank], curIdx: 0, filePath: null, dirty: true })
|
||||||
|
// 初始化配置页「项目信息」的全部字段:项目名称 / 版本号 / JSON 密码恢复默认
|
||||||
|
void get()
|
||||||
|
.updateSettings({ projectName: '新建大数据可视化工程', projectVersion: '1.0.0', jsonPassword: '' })
|
||||||
|
.then((s) =>
|
||||||
|
set({
|
||||||
|
savedInfo: { projectName: s.projectName, projectVersion: s.projectVersion, jsonPassword: s.jsonPassword }
|
||||||
|
})
|
||||||
|
)
|
||||||
toast('已新建工程(默认 1 个空白页面),保存时请选择位置', 'ok')
|
toast('已新建工程(默认 1 个空白页面),保存时请选择位置', 'ok')
|
||||||
},
|
},
|
||||||
loadProject: (payload) => {
|
loadProject: (payload) => {
|
||||||
if (payload?.json) applyProject(get, set, payload.path, payload.json)
|
if (payload?.json) void applyProject(get, set, payload.path, payload.json)
|
||||||
return payload?.mode ?? 'edit'
|
return payload?.mode ?? 'edit'
|
||||||
},
|
},
|
||||||
|
|
||||||
save: async () => {
|
save: async () => {
|
||||||
const { filePath, pages, settings } = get()
|
const { filePath, pages, settings } = get()
|
||||||
// 每保存一次,修订号 +1
|
// 每保存一次,修订号 +1
|
||||||
const newVer = nextPatch(settings?.projectVersion || '1.5.1')
|
const newVer = nextPatch(settings?.projectVersion || '1.5.4')
|
||||||
const meta = { appName: settings?.projectName, version: newVer }
|
const meta = {
|
||||||
|
appName: settings?.projectName ?? '新建大数据可视化工程',
|
||||||
|
version: newVer,
|
||||||
|
projectInfo: {
|
||||||
|
projectName: settings?.projectName ?? '新建大数据可视化工程',
|
||||||
|
projectVersion: newVer
|
||||||
|
}
|
||||||
|
}
|
||||||
const data = JSON.stringify(pagesToJson(pages, meta), null, 2)
|
const data = JSON.stringify(pagesToJson(pages, meta), null, 2)
|
||||||
const pwd = settings?.jsonPassword || undefined
|
const pwd = settings?.jsonPassword || undefined
|
||||||
try {
|
try {
|
||||||
@@ -253,14 +288,31 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
const r = await window.api.project.save(filePath, data, pwd)
|
const r = await window.api.project.save(filePath, data, pwd)
|
||||||
if (r) {
|
if (r) {
|
||||||
await get().updateSettings({ projectVersion: newVer })
|
await get().updateSettings({ projectVersion: newVer })
|
||||||
set({ dirty: false })
|
const s = get().settings
|
||||||
|
set({
|
||||||
|
dirty: false,
|
||||||
|
savedInfo: {
|
||||||
|
projectName: s?.projectName ?? meta.projectInfo.projectName,
|
||||||
|
projectVersion: s?.projectVersion ?? newVer,
|
||||||
|
jsonPassword: s?.jsonPassword ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
toast('工程已保存 · 修订号 v' + newVer, 'ok')
|
toast('工程已保存 · 修订号 v' + newVer, 'ok')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const path = await window.api.project.saveAs(data, pwd)
|
const path = await window.api.project.saveAs(data, pwd)
|
||||||
if (path) {
|
if (path) {
|
||||||
await get().updateSettings({ projectVersion: newVer })
|
await get().updateSettings({ projectVersion: newVer })
|
||||||
set({ filePath: path, dirty: false })
|
const s = get().settings
|
||||||
|
set({
|
||||||
|
filePath: path,
|
||||||
|
dirty: false,
|
||||||
|
savedInfo: {
|
||||||
|
projectName: s?.projectName ?? meta.projectInfo.projectName,
|
||||||
|
projectVersion: s?.projectVersion ?? newVer,
|
||||||
|
jsonPassword: s?.jsonPassword ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
toast('工程已保存为 ' + path.split(/[\\/]/).pop() + ' · 修订号 v' + newVer, 'ok')
|
toast('工程已保存为 ' + path.split(/[\\/]/).pop() + ' · 修订号 v' + newVer, 'ok')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,13 +322,29 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
},
|
},
|
||||||
saveAs: async () => {
|
saveAs: async () => {
|
||||||
const { pages, settings } = get()
|
const { pages, settings } = get()
|
||||||
const meta = { appName: settings?.projectName, version: settings?.projectVersion }
|
const meta = {
|
||||||
|
appName: settings?.projectName ?? '新建大数据可视化工程',
|
||||||
|
version: settings?.projectVersion ?? '1.0.0',
|
||||||
|
projectInfo: {
|
||||||
|
projectName: settings?.projectName ?? '新建大数据可视化工程',
|
||||||
|
projectVersion: settings?.projectVersion ?? '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
const data = JSON.stringify(pagesToJson(pages, meta), null, 2)
|
const data = JSON.stringify(pagesToJson(pages, meta), null, 2)
|
||||||
const pwd = settings?.jsonPassword || undefined
|
const pwd = settings?.jsonPassword || undefined
|
||||||
try {
|
try {
|
||||||
const path = await window.api.project.saveAs(data, pwd)
|
const path = await window.api.project.saveAs(data, pwd)
|
||||||
if (path) {
|
if (path) {
|
||||||
set({ filePath: path, dirty: false })
|
const s = get().settings
|
||||||
|
set({
|
||||||
|
filePath: path,
|
||||||
|
dirty: false,
|
||||||
|
savedInfo: {
|
||||||
|
projectName: s?.projectName ?? meta.projectInfo.projectName,
|
||||||
|
projectVersion: s?.projectVersion ?? meta.projectInfo.projectVersion,
|
||||||
|
jsonPassword: s?.jsonPassword ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
toast('工程已另存为', 'ok')
|
toast('工程已另存为', 'ok')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -291,7 +359,7 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
let r = await window.api.project.open(settings.jsonPassword || undefined)
|
let r = await window.api.project.open(settings.jsonPassword || undefined)
|
||||||
if (!r) return
|
if (!r) return
|
||||||
if (r.json) {
|
if (r.json) {
|
||||||
applyProject(get, set, r.path, r.json)
|
await applyProject(get, set, r.path, r.json)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (r.error === '密码错误') {
|
if (r.error === '密码错误') {
|
||||||
@@ -299,7 +367,7 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
if (!pwd) return
|
if (!pwd) return
|
||||||
r = await window.api.project.openAt(r.path, pwd)
|
r = await window.api.project.openAt(r.path, pwd)
|
||||||
if (r?.json) {
|
if (r?.json) {
|
||||||
applyProject(get, set, r.path, r.json)
|
await applyProject(get, set, r.path, r.json)
|
||||||
// 密码正确后记忆到设置,后续打开免输入
|
// 密码正确后记忆到设置,后续打开免输入
|
||||||
void get().updateSettings({ jsonPassword: pwd })
|
void get().updateSettings({ jsonPassword: pwd })
|
||||||
} else {
|
} else {
|
||||||
@@ -314,6 +382,26 @@ export const useStore = create<AppState>((set, get) => {
|
|||||||
set({ opening: false })
|
set({ opening: false })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
markSaved: () => set({ dirty: false })
|
markSaved: () => set({ dirty: false }),
|
||||||
|
|
||||||
|
loadLastProject: async () => {
|
||||||
|
try {
|
||||||
|
const path = await window.api.project.lastPath()
|
||||||
|
if (!path) return
|
||||||
|
const pwd = get().settings?.jsonPassword || undefined
|
||||||
|
const r = await window.api.project.openAt(path, pwd)
|
||||||
|
if (r?.json) {
|
||||||
|
await applyProject(get, set, r.path, r.json)
|
||||||
|
} else if (r?.error === '密码错误') {
|
||||||
|
toast('上次编辑的工程需要密码,本次加载空白工程(可在“项目/打开”中手动打开)', 'warn')
|
||||||
|
} else {
|
||||||
|
toast('上次编辑的工程文件不可用,已加载空白工程', 'warn')
|
||||||
|
// 记录失效路径,避免每次启动都提示
|
||||||
|
void get().updateSettings({ lastProjectPath: '' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 静默:加载失败时保留空白工程 */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -980,6 +980,33 @@ select {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ 关闭倒计时弹窗 ============ */
|
||||||
|
.quit-modal {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
.quit-count {
|
||||||
|
color: var(--c-accent);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.quit-progress {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.quit-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--c-accent), var(--c-primary-strong));
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-dirty {
|
||||||
|
color: var(--c-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.m-screen-list {
|
.m-screen-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -40,9 +40,16 @@ export interface PageFile {
|
|||||||
PagePreview?: string
|
PagePreview?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectInfo {
|
||||||
|
projectName: string
|
||||||
|
projectVersion: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectJson {
|
export interface ProjectJson {
|
||||||
appName?: string
|
appName?: string
|
||||||
version?: string
|
version?: string
|
||||||
|
/** 项目信息(名称/版本),与工程文件关联,打开时载入、保存时写回 */
|
||||||
|
projectInfo?: ProjectInfo
|
||||||
pages?: PageFile[]
|
pages?: PageFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +71,8 @@ export interface AppSettings {
|
|||||||
publish: PublishDefaults
|
publish: PublishDefaults
|
||||||
/** 上次打开/另存为工程的文件夹(主进程记忆) */
|
/** 上次打开/另存为工程的文件夹(主进程记忆) */
|
||||||
lastDir?: string
|
lastDir?: string
|
||||||
|
/** 上次打开/保存/另存为的工程文件完整路径(启动时自动恢复) */
|
||||||
|
lastProjectPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadPayload {
|
export interface LoadPayload {
|
||||||
|
|||||||
@@ -43,10 +43,15 @@ export function pageSrc(p: Page): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pagesToJson(pages: Page[], meta?: { appName?: string; version?: string }): ProjectJson {
|
export function pagesToJson(
|
||||||
|
pages: Page[],
|
||||||
|
meta: { appName: string; version: string; projectInfo: { projectName: string; projectVersion: string } }
|
||||||
|
): ProjectJson {
|
||||||
return {
|
return {
|
||||||
appName: meta?.appName || '智慧农业大数据可视化控制中心',
|
appName: meta.appName,
|
||||||
version: meta?.version || '1.5.1',
|
version: meta.version,
|
||||||
|
// 项目信息随工程文件一起保存,打开时据此还原
|
||||||
|
projectInfo: { projectName: meta.projectInfo.projectName, projectVersion: meta.projectInfo.projectVersion },
|
||||||
pages: pages.map((p) => ({
|
pages: pages.map((p) => ({
|
||||||
PageID: p.id,
|
PageID: p.id,
|
||||||
PageTitle: p.title,
|
PageTitle: p.title,
|
||||||
@@ -84,20 +89,20 @@ export function nextMajor(v: string): string {
|
|||||||
return String(Number(m[1]) + 1) + m[2]
|
return String(Number(m[1]) + 1) + m[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 版本号:次版本 +1(1.5.1 → 0.3.0,1 → 1.1.0),非数字开头回退 '1.5.1' */
|
/** 版本号:次版本 +1(1.5.1 → 0.3.0,1 → 1.1.0),非数字开头回退 '1.5.4' */
|
||||||
export function nextMinor(v: string): string {
|
export function nextMinor(v: string): string {
|
||||||
const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||||
if (!m) return '1.5.1'
|
if (!m) return '1.5.4'
|
||||||
const major = m[1]
|
const major = m[1]
|
||||||
const minor = m[2] ?? '0'
|
const minor = m[2] ?? '0'
|
||||||
const patch = m[3] ?? '0'
|
const patch = m[3] ?? '0'
|
||||||
return `${major}.${Number(minor) + 1}.${patch}`
|
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' */
|
/** 版本号:修订号 +1(1.5.1 → 0.2.1,0.2 → 0.2.1,1 → 1.0.1),非数字开头回退 '1.5.4' */
|
||||||
export function nextPatch(v: string): string {
|
export function nextPatch(v: string): string {
|
||||||
const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
const m = v.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||||
if (!m) return '1.5.1'
|
if (!m) return '1.5.4'
|
||||||
const major = m[1]
|
const major = m[1]
|
||||||
const minor = m[2] ?? '0'
|
const minor = m[2] ?? '0'
|
||||||
const patch = m[3] ?? '0'
|
const patch = m[3] ?? '0'
|
||||||
@@ -105,10 +110,8 @@ export function nextPatch(v: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function defaultPages(): Page[] {
|
export function defaultPages(): Page[] {
|
||||||
|
// 默认空白工程:不预置示例页面,首次启动 / 无上次工程记录时显示此空白工程
|
||||||
return [
|
return [
|
||||||
{ id: 'P-001', title: '农业数据总览', desc: '全基地农业数据总览', screen: 1, ctrl: 1, display: true, url: 'sample-screen.html', params: 'no=1\ntitle=农业数据总览' },
|
{ id: 'P-001', title: '空白页面', desc: '', screen: 1, ctrl: 0, display: true, url: 'about:blank', params: '' }
|
||||||
{ id: 'P-002', title: '土壤墒情监测', desc: '土壤湿度与墒情分析', screen: 2, ctrl: 2, display: true, url: 'sample-screen.html', params: 'no=2\ntitle=土壤墒情监测' },
|
|
||||||
{ id: 'P-003', title: '温室环境监控', desc: '温室内环境参数实时监控', screen: 3, ctrl: 3, display: true, url: 'sample-screen.html', params: 'no=3\ntitle=温室环境监控' },
|
|
||||||
{ id: 'P-004', title: '设备运维看板', desc: '灌溉设备运维状态', screen: 1, ctrl: 4, display: true, url: 'sample-screen.html', params: 'no=4\ntitle=设备运维看板' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user