diff --git a/SmartAgriCenter/package.json b/SmartAgriCenter/package.json index 886a19c..fbb6a8d 100644 --- a/SmartAgriCenter/package.json +++ b/SmartAgriCenter/package.json @@ -1,6 +1,6 @@ { "name": "smart-agri-center", - "version": "1.5.1", + "version": "1.5.4", "description": "智慧农业大数据可视化控制中心(Electron + React + TypeScript)", "main": "./out/main/index.js", "author": "FanHongCai", diff --git a/SmartAgriCenter/src/main/ipc.ts b/SmartAgriCenter/src/main/ipc.ts index 0dec227..ad9c053 100644 --- a/SmartAgriCenter/src/main/ipc.ts +++ b/SmartAgriCenter/src/main/ipc.ts @@ -129,10 +129,10 @@ export function registerIpc(): void { }) // ===== 工程文件(带 JSON 读写密码)===== - /** 记住用户最后一次选择/保存工程的文件夹 */ - const rememberDir = async (p: string): Promise => { + /** 记住用户最后一次选择/保存的工程(文件夹 + 文件完整路径,供启动时自动恢复) */ + const rememberProject = async (p: string): Promise => { try { - await saveSettings({ lastDir: dirname(p) }) + await saveSettings({ lastDir: dirname(p), lastProjectPath: p }) } catch { // 记录失败不影响主流程 } @@ -151,7 +151,7 @@ export function registerIpc(): void { }) if (r.canceled || !r.filePath) return null 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 }) @@ -160,6 +160,7 @@ export function registerIpc(): void { const settings = await loadSettings() const pwd = payload.password ?? settings.jsonPassword const res = await writeProjectFile(payload.path, payload.data, pwd) + if (res.ok) await rememberProject(payload.path) return res.ok ? payload.path : null }) @@ -175,14 +176,19 @@ export function registerIpc(): void { if (r.canceled || !r.filePaths[0]) return null const pwd = password ?? settings.jsonPassword const res = await readProjectFile(r.filePaths[0], pwd) - await rememberDir(r.filePaths[0]) + await rememberProject(r.filePaths[0]) return res }) - // 打开指定路径工程(用于密码输入重试 / 快捷方式场景) - ipcMain.handle('project:openAt', (_e, payload: { path: string; password?: string }) => - readProjectFile(payload.path, payload.password ?? '') - ) + // 打开指定路径工程(用于密码输入重试 / 快捷方式场景 / 启动自动恢复) + ipcMain.handle('project:openAt', async (_e, payload: { path: string; password?: string }) => { + 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()) @@ -328,32 +334,41 @@ export function registerIpc(): void { let dirty = false let forceQuit = false +/** 关闭倒计时秒数:到点后自动退出主程序 */ +const QUIT_COUNTDOWN = 10 +let quitGuardTimer: NodeJS.Timeout | null = null function setDirtyFlag(d: boolean): void { dirty = d } 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) => { if (forceQuit) return e.preventDefault() - const warn = dirty ? '当前工程有未保存的修改。\n\n' : '' - void dialog - .showMessageBox(win, { - type: 'warning', - title: '确认退出', - message: `${warn}确定要退出智慧农业大数据可视化控制中心吗?`, - buttons: ['退出', '取消'], - defaultId: 1, - cancelId: 1 - }) - .then((r) => { - if (r.response === 0) { - forceQuit = true - // 先关闭所有播放中的子页面(屏幕窗体/轮播/探测/主屏播放),再关闭主窗口 - closeAllScreens() - win.close() - } - }) + // 通知渲染层弹出倒计时确认框(由渲染层决定是否退出) + if (!win.isDestroyed()) win.webContents.send('app:requestQuit') + // 兜底:若渲染层在倒计时结束后仍未响应,则强制退出,避免卡死 + if (quitGuardTimer) clearTimeout(quitGuardTimer) + quitGuardTimer = setTimeout( + () => { + forceQuit = true + closeAllScreens() + if (!win.isDestroyed()) win.close() + }, + (QUIT_COUNTDOWN + 5) * 1000 + ) }) } diff --git a/SmartAgriCenter/src/main/settings.ts b/SmartAgriCenter/src/main/settings.ts index 3f17735..4c5e7b7 100644 --- a/SmartAgriCenter/src/main/settings.ts +++ b/SmartAgriCenter/src/main/settings.ts @@ -25,16 +25,19 @@ export interface AppSettings { publish: PublishDefaults /** 上次打开/另存为工程的文件夹,用于记住上次位置 */ lastDir: string + /** 上次打开/保存/另存为的工程文件完整路径,用于启动时自动恢复 */ + lastProjectPath: string } const DEFAULTS: AppSettings = { projectName: '智慧农业大数据可视化控制中心', - projectVersion: '1.5.1', + projectVersion: '1.5.4', jsonPassword: '', keyThreshold: 350, keyLockKey: 'F9', publish: { mode: 'multi', screenIndex: 1, alsoMain: false, intervalSec: 10 }, - lastDir: '' + lastDir: '', + lastProjectPath: '' } let cache: AppSettings | null = null diff --git a/SmartAgriCenter/src/preload/index.d.ts b/SmartAgriCenter/src/preload/index.d.ts index d44ff57..e04f262 100644 --- a/SmartAgriCenter/src/preload/index.d.ts +++ b/SmartAgriCenter/src/preload/index.d.ts @@ -107,6 +107,8 @@ export interface SmartAgriApi { save(path: string, data: string, password?: string): Promise 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 }> + /** 返回上次编辑的工程文件路径(启动自动恢复用),无则返回 null */ + lastPath(): Promise } settings: { get(): Promise @@ -119,6 +121,10 @@ export interface SmartAgriApi { info(): Promise consumeLoad(): Promise onLoadProject(cb: (payload: LoadPayload) => void): () => void + /** 主进程拦截关闭时触发,渲染层应弹出倒计时确认框 */ + onRequestQuit(cb: () => void): () => void + /** 倒计时结束或用户确认后,通知主进程强制退出 */ + quitForce(): void } key: { onScreenKey(cb: (n: number) => void): () => void diff --git a/SmartAgriCenter/src/preload/index.ts b/SmartAgriCenter/src/preload/index.ts index 0aa9ce8..b37fa50 100644 --- a/SmartAgriCenter/src/preload/index.ts +++ b/SmartAgriCenter/src/preload/index.ts @@ -54,7 +54,8 @@ const api = { open: (password?: string): Promise<{ path: string; json: unknown | null; error?: string } | null> => ipcRenderer.invoke('project:open', password), 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 => ipcRenderer.invoke('project:lastPath') }, settings: { get: (): Promise => ipcRenderer.invoke('settings:get'), @@ -66,7 +67,9 @@ const api = { app: { info: (): Promise => ipcRenderer.invoke('app:info'), consumeLoad: (): Promise => 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: { onScreenKey: (cb: (n: number) => void): (() => void) => on('key:switch', cb) diff --git a/SmartAgriCenter/src/renderer/src/App.tsx b/SmartAgriCenter/src/renderer/src/App.tsx index 0753c8f..facef3d 100644 --- a/SmartAgriCenter/src/renderer/src/App.tsx +++ b/SmartAgriCenter/src/renderer/src/App.tsx @@ -4,6 +4,7 @@ import Ribbon from './components/Ribbon' import StatusBar from './components/StatusBar' import Toast from './components/Toast' import { PasswordModal } from './components/PasswordModal' +import { QuitConfirmModal } from './components/QuitConfirmModal' import EditView from './views/EditView' import ConfigView from './views/ConfigView' import { useStore } from './store' @@ -16,6 +17,7 @@ export default function App(): React.JSX.Element { const view = useStore((s) => s.view) const pages = useStore((s) => s.pages) const [configMenu, setConfigMenu] = useState('info') + const [quitOpen, setQuitOpen] = useState(false) // 监听屏幕状态变化 useEffect(() => { @@ -40,21 +42,35 @@ export default function App(): React.JSX.Element { ) }, [pages]) - // 启动:加载应用设置 + 消费/监听快捷方式打开的项目 + // 启动:加载应用设置 + 消费/监听快捷方式打开的项目;无快捷方式工程时自动恢复上次编辑的工程 useEffect(() => { const st = useStore.getState - void st().loadSettings() const handle = (p: unknown): void => { const lp = p as LoadPayload | null if (!lp?.json) return const mode = st().loadProject(lp) 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) return off }, []) + // 主进程拦截关闭 → 弹出倒计时 / 保存确认框 + useEffect(() => { + const off = window.api.app.onRequestQuit(() => setQuitOpen(true)) + return off + }, []) + // 主窗口数字键切换页面 useEffect(() => { const h = (e: KeyboardEvent): void => { @@ -86,6 +102,31 @@ export default function App(): React.JSX.Element { + {quitOpen && ( + { + 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)} + /> + )} ) } diff --git a/SmartAgriCenter/src/renderer/src/components/QuitConfirmModal.tsx b/SmartAgriCenter/src/renderer/src/components/QuitConfirmModal.tsx new file mode 100644 index 0000000..eab125b --- /dev/null +++ b/SmartAgriCenter/src/renderer/src/components/QuitConfirmModal.tsx @@ -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 ( +
+
+
+ {hasUnsaved ? '保存更改' : '正在退出'} +
+
+ {hasUnsaved ? ( +

项目有未保存的修改,是否先保存工程后再退出?

+ ) : ( + <> +

+ 客户端将在 {left} 秒后自动关闭。 +

+
+
+
+

如有未保存的修改,请先取消并保存工程。

+ + )} +
+
+ {hasUnsaved ? ( + <> + + + + + ) : ( + <> + + + + )} +
+
+
+ ) +} diff --git a/SmartAgriCenter/src/renderer/src/components/config/AboutPanel.tsx b/SmartAgriCenter/src/renderer/src/components/config/AboutPanel.tsx index e326d8c..4754967 100644 --- a/SmartAgriCenter/src/renderer/src/components/config/AboutPanel.tsx +++ b/SmartAgriCenter/src/renderer/src/components/config/AboutPanel.tsx @@ -16,7 +16,7 @@ export default function AboutPanel(): React.JSX.Element {
🌾
{settings?.projectName ?? '智慧农业大数据可视化控制中心'}
- Version {settings?.projectVersion ?? '1.5.1'} + Version {settings?.projectVersion ?? '1.5.4'} {info?.appVersion ? ` · 安装包 ${info.appVersion}` : ''}

diff --git a/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx b/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx index fa431e0..89d81b4 100644 --- a/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx +++ b/SmartAgriCenter/src/renderer/src/components/config/ProjectInfo.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { useStore } from '../../store' import { toast } from '../../toast' import { nextMinor } from '../../utils' @@ -8,7 +8,13 @@ export default function ProjectInfo(): React.JSX.Element { const pages = useStore((s) => s.pages) const screens = useStore((s) => s.screens) 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 [name, setName] = useState(settings?.projectName ?? '') @@ -16,20 +22,26 @@ export default function ProjectInfo(): React.JSX.Element { const [pwd, setPwd] = useState(settings?.jsonPassword ?? '') const [showPwd, setShowPwd] = useState(false) - // 设置异步加载完成后同步一次 - const synced = useRef(false) + // 跟随设置变化同步字段(新建/打开工程初始化项目信息时立即刷新) useEffect(() => { - if (settings && !synced.current) { - synced.current = true - setName(settings.projectName) - setVer(settings.projectVersion) - setPwd(settings.jsonPassword) - } + if (!settings) return + setName(settings.projectName) + setVer(settings.projectVersion) + setPwd(settings.jsonPassword) }, [settings]) - const save = async (): Promise => { - await st().updateSettings({ projectName: name.trim() || '智慧农业大数据可视化控制中心', projectVersion: ver.trim() || '1.5.1', jsonPassword: pwd }) - toast('项目信息与 JSON 密码已保存', 'ok') + // 项目信息实时同步到设置(关闭系统时会提示是否保存) + const syncName = (v: string): void => { + 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 => { @@ -43,7 +55,7 @@ export default function ProjectInfo(): React.JSX.Element { } const bumpMinor = async (): Promise => { - const nv = nextMinor(ver || '1.5.1') + const nv = nextMinor(ver || '1.0.0') setVer(nv) await st().updateSettings({ projectVersion: nv }) toast('次版本 +1 → ' + nv, 'ok') @@ -56,12 +68,12 @@ export default function ProjectInfo(): React.JSX.Element {

- setName(e.target.value)} placeholder="例如:智慧农业大数据可视化控制中心" /> + syncName(e.target.value)} placeholder="例如:智慧农业大数据可视化控制中心" />
- setVer(e.target.value)} placeholder="例如:1.5.1" /> + syncVer(e.target.value)} placeholder="例如:1.0.0" /> @@ -84,7 +96,7 @@ export default function ProjectInfo(): React.JSX.Element { className="cfg-input" type={showPwd ? 'text' : 'password'} value={pwd} - onChange={(e) => setPwd(e.target.value)} + onChange={(e) => syncPwd(e.target.value)} placeholder="留空则不加密" />
-
- - - - -
+ {infoDirty &&

项目信息已修改,关闭系统时将提示是否保存。

}
diff --git a/SmartAgriCenter/src/renderer/src/store.ts b/SmartAgriCenter/src/renderer/src/store.ts index b5ab9f4..261f095 100644 --- a/SmartAgriCenter/src/renderer/src/store.ts +++ b/SmartAgriCenter/src/renderer/src/store.ts @@ -13,6 +13,8 @@ interface AppState { screens: ScreenInfo[] zoom: number settings: AppSettings | null + /** 上次「保存/打开/新建工程」时的项目信息快照,用于派生项目信息是否已变更 */ + savedInfo: { projectName: string; projectVersion: string; jsonPassword: string } /** 工程文件操作进行中(打开/另存等),用于防抖防重入 */ opening: boolean /** 编辑区预览 webview 引用(用于截图捕获实际预览内容) */ @@ -33,6 +35,8 @@ interface AppState { setZoom: (z: number) => void loadSettings: () => Promise updateSettings: (patch: Partial) => Promise + /** 启动时自动恢复上次编辑的工程(无则保留示例工程) */ + loadLastProject: () => Promise newProject: () => void loadProject: (payload: LoadPayload) => 'edit' | 'publish' save: () => Promise @@ -57,10 +61,25 @@ function blankPage(pages: Page[], id: string, title: string): Page { } } -/** 应用工程 JSON 到编辑状态 */ -function applyProject(get: () => AppState, set: (p: Partial) => void, path: string, json: ProjectJson): void { +/** 应用工程 JSON 到编辑状态(页面 + 项目信息随工程一起载入配置页) */ +async function applyProject(get: () => AppState, set: (p: Partial) => void, path: string, json: ProjectJson): Promise { 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 { window.api.syncProject(pages.map((p) => ({ ctrl: p.ctrl, title: p.title, url: pageSrc(p) }))) } catch { @@ -81,6 +100,7 @@ export const useStore = create((set, get) => { screens: [], zoom: 100, settings: null, + savedInfo: { projectName: '', projectVersion: '', jsonPassword: '' }, previewWv: null, opening: false, @@ -234,18 +254,33 @@ export const useStore = create((set, get) => { params: '' } 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') }, 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' }, save: async () => { const { filePath, pages, settings } = get() // 每保存一次,修订号 +1 - const newVer = nextPatch(settings?.projectVersion || '1.5.1') - const meta = { appName: settings?.projectName, version: newVer } + const newVer = nextPatch(settings?.projectVersion || '1.5.4') + const meta = { + appName: settings?.projectName ?? '新建大数据可视化工程', + version: newVer, + projectInfo: { + projectName: settings?.projectName ?? '新建大数据可视化工程', + projectVersion: newVer + } + } const data = JSON.stringify(pagesToJson(pages, meta), null, 2) const pwd = settings?.jsonPassword || undefined try { @@ -253,14 +288,31 @@ export const useStore = create((set, get) => { const r = await window.api.project.save(filePath, data, pwd) if (r) { 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') } } else { const path = await window.api.project.saveAs(data, pwd) if (path) { 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') } } @@ -270,13 +322,29 @@ export const useStore = create((set, get) => { }, saveAs: async () => { 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 pwd = settings?.jsonPassword || undefined try { const path = await window.api.project.saveAs(data, pwd) 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') } } catch (err) { @@ -291,7 +359,7 @@ export const useStore = create((set, get) => { let r = await window.api.project.open(settings.jsonPassword || undefined) if (!r) return if (r.json) { - applyProject(get, set, r.path, r.json) + await applyProject(get, set, r.path, r.json) return } if (r.error === '密码错误') { @@ -299,7 +367,7 @@ export const useStore = create((set, get) => { if (!pwd) return r = await window.api.project.openAt(r.path, pwd) if (r?.json) { - applyProject(get, set, r.path, r.json) + await applyProject(get, set, r.path, r.json) // 密码正确后记忆到设置,后续打开免输入 void get().updateSettings({ jsonPassword: pwd }) } else { @@ -314,6 +382,26 @@ export const useStore = create((set, get) => { 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 { + /* 静默:加载失败时保留空白工程 */ + } + } } }) diff --git a/SmartAgriCenter/src/renderer/src/styles/global.css b/SmartAgriCenter/src/renderer/src/styles/global.css index e5050a3..8f4d5f5 100644 --- a/SmartAgriCenter/src/renderer/src/styles/global.css +++ b/SmartAgriCenter/src/renderer/src/styles/global.css @@ -980,6 +980,33 @@ select { font-size: 12px; 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 { display: flex; flex-direction: column; diff --git a/SmartAgriCenter/src/renderer/src/types.ts b/SmartAgriCenter/src/renderer/src/types.ts index 424ec86..2273cee 100644 --- a/SmartAgriCenter/src/renderer/src/types.ts +++ b/SmartAgriCenter/src/renderer/src/types.ts @@ -40,9 +40,16 @@ export interface PageFile { PagePreview?: string } +export interface ProjectInfo { + projectName: string + projectVersion: string +} + export interface ProjectJson { appName?: string version?: string + /** 项目信息(名称/版本),与工程文件关联,打开时载入、保存时写回 */ + projectInfo?: ProjectInfo pages?: PageFile[] } @@ -64,6 +71,8 @@ export interface AppSettings { publish: PublishDefaults /** 上次打开/另存为工程的文件夹(主进程记忆) */ lastDir?: string + /** 上次打开/保存/另存为的工程文件完整路径(启动时自动恢复) */ + lastProjectPath?: string } export interface LoadPayload { diff --git a/SmartAgriCenter/src/renderer/src/utils.ts b/SmartAgriCenter/src/renderer/src/utils.ts index be991c8..795aa39 100644 --- a/SmartAgriCenter/src/renderer/src/utils.ts +++ b/SmartAgriCenter/src/renderer/src/utils.ts @@ -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 { - appName: meta?.appName || '智慧农业大数据可视化控制中心', - version: meta?.version || '1.5.1', + appName: meta.appName, + version: meta.version, + // 项目信息随工程文件一起保存,打开时据此还原 + projectInfo: { projectName: meta.projectInfo.projectName, projectVersion: meta.projectInfo.projectVersion }, pages: pages.map((p) => ({ PageID: p.id, PageTitle: p.title, @@ -84,20 +89,20 @@ export function nextMajor(v: string): string { 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 { 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 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' */ +/** 版本号:修订号 +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 { 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 minor = m[2] ?? '0' const patch = m[3] ?? '0' @@ -105,10 +110,8 @@ export function nextPatch(v: string): string { } export function defaultPages(): Page[] { + // 默认空白工程:不预置示例页面,首次启动 / 无上次工程记录时显示此空白工程 return [ - { id: 'P-001', title: '农业数据总览', desc: '全基地农业数据总览', screen: 1, ctrl: 1, display: true, url: 'sample-screen.html', params: 'no=1\ntitle=农业数据总览' }, - { 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=设备运维看板' } + { id: 'P-001', title: '空白页面', desc: '', screen: 1, ctrl: 0, display: true, url: 'about:blank', params: '' } ] } diff --git a/SmartAgriCenter/智慧农业大数据可视化控制中心(1.3.2).json b/SmartAgriCenter/会东智慧蚕桑大数据中心_控制中心(1.3.2).json similarity index 100% rename from SmartAgriCenter/智慧农业大数据可视化控制中心(1.3.2).json rename to SmartAgriCenter/会东智慧蚕桑大数据中心_控制中心(1.3.2).json