From 4a29598037628b81d137d531dc81101c32db4382 Mon Sep 17 00:00:00 2001 From: Garfield Lee Date: Sun, 22 Mar 2026 04:39:46 +0800 Subject: [PATCH] feat(stage-*): refactor settings data page and add app data folder action (#1447) ------ Co-authored-by-agent: Codex --- apps/stage-tamagotchi/electron.vite.config.ts | 1 + .../src/main/services/electron/app.test.ts | 59 ++++ .../src/main/services/electron/app.ts | 12 +- .../components/desktop-folder-section.vue | 51 ++++ .../data/components/desktop-reset-section.vue | 49 +++ .../renderer/pages/settings/data/index.vue | 38 +++ apps/stage-tamagotchi/src/shared/eventa.ts | 1 + apps/stage-tamagotchi/tsconfig.web.json | 6 + packages/i18n/src/locales/en/settings.yaml | 4 + .../i18n/src/locales/zh-Hans/settings.yaml | 4 + packages/stage-pages/package.json | 3 +- .../data/components/chats-section.vue | 111 +++++++ .../data/components/danger-section.vue | 97 ++++++ .../components/models-modules-section.vue | 84 ++++++ .../data/components/status-banner.vue | 23 ++ .../src/pages/settings/data/index.vue | 283 +----------------- .../src/pages/settings/data/status.ts | 47 +++ 17 files changed, 598 insertions(+), 275 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/services/electron/app.test.ts create mode 100644 apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-folder-section.vue create mode 100644 apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-reset-section.vue create mode 100644 apps/stage-tamagotchi/src/renderer/pages/settings/data/index.vue create mode 100644 packages/stage-pages/src/pages/settings/data/components/chats-section.vue create mode 100644 packages/stage-pages/src/pages/settings/data/components/danger-section.vue create mode 100644 packages/stage-pages/src/pages/settings/data/components/models-modules-section.vue create mode 100644 packages/stage-pages/src/pages/settings/data/components/status-banner.vue create mode 100644 packages/stage-pages/src/pages/settings/data/status.ts diff --git a/apps/stage-tamagotchi/electron.vite.config.ts b/apps/stage-tamagotchi/electron.vite.config.ts index 86469dded..d4e2a88b7 100644 --- a/apps/stage-tamagotchi/electron.vite.config.ts +++ b/apps/stage-tamagotchi/electron.vite.config.ts @@ -207,6 +207,7 @@ export default defineConfig({ exclude: base => [ ...base, '**/settings/connection/index.vue', + '**/settings/data/index.vue', '**/settings/system/general.vue', '**/settings/modules/mcp.vue', ], diff --git a/apps/stage-tamagotchi/src/main/services/electron/app.test.ts b/apps/stage-tamagotchi/src/main/services/electron/app.test.ts new file mode 100644 index 000000000..e22178914 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/electron/app.test.ts @@ -0,0 +1,59 @@ +import { createContext, defineInvoke } from '@moeru/eventa' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { electronAppOpenUserDataFolder } from '../../../shared/eventa' +import { createAppService } from './app' + +const appMock = vi.hoisted(() => ({ + getPath: vi.fn(), + quit: vi.fn(), +})) + +const shellMock = vi.hoisted(() => ({ + openPath: vi.fn(), +})) + +vi.mock('electron', () => ({ + app: appMock, + shell: shellMock, +})) + +vi.mock('std-env', () => ({ + isLinux: false, + isMacOS: false, + isWindows: true, +})) + +describe('createAppService', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('opens the Electron userData folder and returns its path', async () => { + const context = createContext() + appMock.getPath.mockReturnValue('/tmp/airi-user-data') + shellMock.openPath.mockResolvedValue('') + + createAppService({ context: context as never, window: {} as never }) + + const openUserDataFolder = defineInvoke(context, electronAppOpenUserDataFolder) + + await expect(openUserDataFolder()).resolves.toEqual({ path: '/tmp/airi-user-data' }) + expect(appMock.getPath).toHaveBeenCalledWith('userData') + expect(shellMock.openPath).toHaveBeenCalledWith('/tmp/airi-user-data') + }) + + it('throws when Electron fails to open the userData folder', async () => { + const context = createContext() + appMock.getPath.mockReturnValue('/tmp/airi-user-data') + shellMock.openPath.mockResolvedValue('Failed to open path') + + createAppService({ context: context as never, window: {} as never }) + + const openUserDataFolder = defineInvoke(context, electronAppOpenUserDataFolder) + + await expect(openUserDataFolder()).rejects.toThrow('Failed to open path') + expect(appMock.getPath).toHaveBeenCalledWith('userData') + expect(shellMock.openPath).toHaveBeenCalledWith('/tmp/airi-user-data') + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/electron/app.ts b/apps/stage-tamagotchi/src/main/services/electron/app.ts index 8daab8dc7..94e445ddc 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/app.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/app.ts @@ -2,14 +2,22 @@ import type { createContext } from '@moeru/eventa/adapters/electron/main' import type { BrowserWindow } from 'electron' import { defineInvokeHandler } from '@moeru/eventa' -import { app } from 'electron' +import { app, shell } from 'electron' import { isLinux, isMacOS, isWindows } from 'std-env' -import { electron, electronAppQuit } from '../../../shared/eventa' +import { electron, electronAppOpenUserDataFolder, electronAppQuit } from '../../../shared/eventa' export function createAppService(params: { context: ReturnType['context'], window: BrowserWindow }) { defineInvokeHandler(params.context, electron.app.isMacOS, () => isMacOS) defineInvokeHandler(params.context, electron.app.isWindows, () => isWindows) defineInvokeHandler(params.context, electron.app.isLinux, () => isLinux) + defineInvokeHandler(params.context, electronAppOpenUserDataFolder, async () => { + const path = app.getPath('userData') + const openResult = await shell.openPath(path) + if (openResult) { + throw new Error(openResult) + } + return { path } + }) defineInvokeHandler(params.context, electronAppQuit, () => app.quit()) } diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-folder-section.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-folder-section.vue new file mode 100644 index 000000000..3ab32857d --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-folder-section.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-reset-section.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-reset-section.vue new file mode 100644 index 000000000..069f7a2fd --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/data/components/desktop-reset-section.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/data/index.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/data/index.vue new file mode 100644 index 000000000..d075691c4 --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/data/index.vue @@ -0,0 +1,38 @@ + + + + + +meta: + layout: settings + titleKey: settings.pages.data.title + subtitleKey: settings.title + descriptionKey: settings.pages.data.description + icon: i-solar:database-bold-duotone + settingsEntry: true + order: 7 + stageTransition: + name: slide + pageSpecificAvailable: true + diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 3a3ea5ef5..24a6b1cdd 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -226,6 +226,7 @@ export interface ElectronWindowLifecycleState { export const electronWindowLifecycleChanged = defineEventa('eventa:event:electron:window:lifecycle-changed') export const electronGetWindowLifecycleState = defineInvokeEventa('eventa:invoke:electron:window:get-lifecycle-state') export const electronWindowSetAlwaysOnTop = defineInvokeEventa('eventa:invoke:electron:window:set-always-on-top') +export const electronAppOpenUserDataFolder = defineInvokeEventa<{ path: string }>('eventa:invoke:electron:app:open-user-data-folder') export const electronAppQuit = defineInvokeEventa('eventa:invoke:electron:app:quit') export type StageThreeRuntimeTraceEnvelope diff --git a/apps/stage-tamagotchi/tsconfig.web.json b/apps/stage-tamagotchi/tsconfig.web.json index dfc56acad..8dfa8ae0c 100644 --- a/apps/stage-tamagotchi/tsconfig.web.json +++ b/apps/stage-tamagotchi/tsconfig.web.json @@ -13,6 +13,12 @@ "useDefineForClassFields": true, "baseUrl": "./src/renderer", "paths": { + "@proj-airi/stage-pages/*.vue": [ + "../../packages/stage-pages/src/*.vue" + ], + "@proj-airi/stage-pages/*": [ + "../../packages/stage-pages/src/*.ts" + ], "@proj-airi/stage-ui/*": [ "../../packages/stage-ui/src/*" ], diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index edd7f1940..51254091c 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -224,6 +224,10 @@ pages: title: Delete all data description: Wipe every local setting, provider config, and model. delete: Delete all data + desktop-folder: + title: Open app data folder + description: Open AIRI's data folder in your file manager. + open: Open folder desktop: title: Reset desktop settings & states description: Clear AIRI desktop settings and runtime state. diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index 51bfe4c8f..c38379984 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -217,6 +217,10 @@ pages: title: 删除所有数据 description: 清除每个本地设置、提供商配置和模型。 delete: 删除所有数据 + desktop-folder: + title: 打开应用数据文件夹 + description: 在文件管理器中打开 AIRI 的数据文件夹。 + open: 打开文件夹 desktop: title: 重置桌面设置和状态 description: 清除 AIRI 桌面设置和运行状态。 diff --git a/packages/stage-pages/package.json b/packages/stage-pages/package.json index 1230b7bac..6f469239c 100644 --- a/packages/stage-pages/package.json +++ b/packages/stage-pages/package.json @@ -15,7 +15,8 @@ "directory": "packages/stage-pages" }, "exports": { - "./*": "./src/*" + "./*.vue": "./src/*.vue", + "./*": "./src/*.ts" }, "scripts": { "typecheck": "vue-tsc --noEmit" diff --git a/packages/stage-pages/src/pages/settings/data/components/chats-section.vue b/packages/stage-pages/src/pages/settings/data/components/chats-section.vue new file mode 100644 index 000000000..3093bee83 --- /dev/null +++ b/packages/stage-pages/src/pages/settings/data/components/chats-section.vue @@ -0,0 +1,111 @@ + + + diff --git a/packages/stage-pages/src/pages/settings/data/components/danger-section.vue b/packages/stage-pages/src/pages/settings/data/components/danger-section.vue new file mode 100644 index 000000000..228813cb0 --- /dev/null +++ b/packages/stage-pages/src/pages/settings/data/components/danger-section.vue @@ -0,0 +1,97 @@ + + + diff --git a/packages/stage-pages/src/pages/settings/data/components/models-modules-section.vue b/packages/stage-pages/src/pages/settings/data/components/models-modules-section.vue new file mode 100644 index 000000000..f717dbaea --- /dev/null +++ b/packages/stage-pages/src/pages/settings/data/components/models-modules-section.vue @@ -0,0 +1,84 @@ + + + diff --git a/packages/stage-pages/src/pages/settings/data/components/status-banner.vue b/packages/stage-pages/src/pages/settings/data/components/status-banner.vue new file mode 100644 index 000000000..c226ec9a6 --- /dev/null +++ b/packages/stage-pages/src/pages/settings/data/components/status-banner.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/stage-pages/src/pages/settings/data/index.vue b/packages/stage-pages/src/pages/settings/data/index.vue index 410dad519..c51900edc 100644 --- a/packages/stage-pages/src/pages/settings/data/index.vue +++ b/packages/stage-pages/src/pages/settings/data/index.vue @@ -1,281 +1,20 @@ diff --git a/packages/stage-pages/src/pages/settings/data/status.ts b/packages/stage-pages/src/pages/settings/data/status.ts new file mode 100644 index 000000000..deb772e2f --- /dev/null +++ b/packages/stage-pages/src/pages/settings/data/status.ts @@ -0,0 +1,47 @@ +import { errorMessageFrom } from '@moeru/std' +import { shallowRef } from 'vue' + +export type DataSettingsStatusTone = 'neutral' | 'success' | 'error' + +export interface DataSettingsStatusPayload { + message: string + tone: DataSettingsStatusTone +} + +export interface DataSettingsStatusEmits { + status: [payload: DataSettingsStatusPayload] +} + +export type DataSettingsStatusEmit = (event: 'status', payload: DataSettingsStatusPayload) => void + +export function createDataSettingsStatusHelpers(emit: DataSettingsStatusEmit) { + function emitStatus(message: string, tone: DataSettingsStatusTone = 'success') { + emit('status', { message, tone }) + } + + function handleActionError(error: unknown) { + console.error(error) + emitStatus(errorMessageFrom(error) ?? 'Unknown error', 'error') + } + + return { + emitStatus, + handleActionError, + } +} + +export function createDataSettingsStatusState() { + const statusMessage = shallowRef('') + const statusTone = shallowRef('neutral') + + function handleStatus(payload: DataSettingsStatusPayload) { + statusMessage.value = payload.message + statusTone.value = payload.tone + } + + return { + statusMessage, + statusTone, + handleStatus, + } +}