diff --git a/dashboard/src/components/config/UnsavedChangesConfirmDialog.vue b/dashboard/src/components/config/UnsavedChangesConfirmDialog.vue new file mode 100644 index 000000000..f81f1167f --- /dev/null +++ b/dashboard/src/components/config/UnsavedChangesConfirmDialog.vue @@ -0,0 +1,98 @@ + + + + + + diff --git a/dashboard/src/i18n/locales/en-US/features/config.json b/dashboard/src/i18n/locales/en-US/features/config.json index 5f3af4135..4b726ae3c 100644 --- a/dashboard/src/i18n/locales/en-US/features/config.json +++ b/dashboard/src/i18n/locales/en-US/features/config.json @@ -112,5 +112,18 @@ "addToConfig": "Added to config", "fileCount": "Files: {count}", "done": "Done" + }, + "unsavedChangesWarning": { + "dialogTitle": "Unsaved changes", + "leavePage": "You have unsaved changes. Do you want to save before leaving?", + "switchConfig": "Switching config will discard unsaved changes. Do you want to save first?", + "options": { + "save": "Save", + "saveAndSwitch": "Save and switch", + "discardAndSwitch": "Discard changes and switch", + "closeCard": "Close the pop-up window", + "confirm": "confirm", + "cancel": "cancel" + } } } diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index 39564a717..e7cd90408 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -112,5 +112,18 @@ "addToConfig": "已加入配置", "fileCount": "文件:{count}", "done": "完成" + }, + "unsavedChangesWarning": { + "dialogTitle": "未保存的更改", + "leavePage": "当前配置有未保存的更改,切换前是否保存?", + "switchConfig": "切换配置文件会丢失当前未保存的更改,是否先保存?", + "options": { + "save": "保存", + "saveAndSwitch": "保存并切换", + "discardAndSwitch": "放弃更改并切换", + "closeCard": "关闭弹窗", + "confirm": "确定", + "cancel": "取消" + } } } diff --git a/dashboard/src/views/ConfigPage.vue b/dashboard/src/views/ConfigPage.vue index fee392554..7c50fbb58 100644 --- a/dashboard/src/views/ConfigPage.vue +++ b/dashboard/src/views/ConfigPage.vue @@ -7,7 +7,7 @@
- @@ -191,6 +191,10 @@
+ + + + @@ -206,6 +210,7 @@ import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog'; +import UnsavedChangesConfirmDialog from '@/components/config/UnsavedChangesConfirmDialog.vue'; export default { name: 'ConfigPage', @@ -213,7 +218,8 @@ export default { AstrBotCoreConfigWrapper, VueMonacoEditor, WaitingForRestart, - StandaloneChat + StandaloneChat, + UnsavedChangesConfirmDialog }, props: { initialConfigId: { @@ -233,6 +239,40 @@ export default { }; }, +// 检查未保存的更改 + async beforeRouteLeave(to, from, next) { + if (this.hasUnsavedChanges) { + const confirmed = await this.$refs.unsavedChangesDialog?.open({ + title: this.tm('unsavedChangesWarning.dialogTitle'), + message: this.tm('unsavedChangesWarning.leavePage'), + confirmHint: `${this.tm('unsavedChangesWarning.options.saveAndSwitch')}:${this.tm('unsavedChangesWarning.options.confirm')}`, + cancelHint: `${this.tm('unsavedChangesWarning.options.discardAndSwitch')}:${this.tm('unsavedChangesWarning.options.cancel')}`, + closeHint: `${this.tm('unsavedChangesWarning.options.closeCard')}:"x"` + }); + // 关闭弹窗不跳转 + if (confirmed === 'close') { + next(false); + } else if (confirmed) { + const result = await this.updateConfig(); + if (this.isSystemConfig) { + next(false); + } else { + if (result?.success) { + await new Promise(resolve => setTimeout(resolve, 800)); + next(); + } else { + next(false); + } + } + } else { + this.hasUnsavedChanges = false; + next(); + } + } else { + next(); + } + }, + computed: { messages() { return { @@ -243,6 +283,11 @@ export default { configApplyError: this.tm('messages.configApplyError') }; }, + // 检查配置是否变化 + configHasChanges() { + if (!this.originalConfigData || !this.config_data) return false; + return JSON.stringify(this.originalConfigData) !== JSON.stringify(this.config_data); + }, configInfoNameList() { return this.configInfoList.map(info => info.name); }, @@ -269,8 +314,16 @@ export default { config_data_str(val) { this.config_data_has_changed = true; }, - '$route.fullPath'(newVal) { - this.syncConfigTypeFromHash(newVal); + config_data: { + deep: true, + handler() { + if (this.fetched) { + this.hasUnsavedChanges = this.configHasChanges; + } + } + }, + async '$route.fullPath'(newVal) { + await this.syncConfigTypeFromHash(newVal); }, initialConfigId(newVal) { if (!newVal) { @@ -309,6 +362,7 @@ export default { // 多配置文件管理 selectedConfigID: null, // 用于存储当前选中的配置项信息 + currentConfigId: null, // 跟踪当前正在编辑的配置id configInfoList: [], configFormData: { name: '', @@ -318,6 +372,11 @@ export default { // 测试聊天 testChatDrawer: false, testConfigId: null, + + // 未保存的更改状态 + hasUnsavedChanges: false, + // 存储原始配置 + originalConfigData: null, } }, mounted() { @@ -334,6 +393,13 @@ export default { // 监听语言切换事件,重新加载配置以获取插件的 i18n 数据 window.addEventListener('astrbot-locale-changed', this.handleLocaleChange); + + // 保存初始配置 + this.$watch('config_data', (newVal) => { + if (!this.originalConfigData && newVal) { + this.originalConfigData = JSON.parse(JSON.stringify(newVal)); + } + }, { immediate: false, deep: true }); }, beforeUnmount() { @@ -362,14 +428,14 @@ export default { const cleanHash = rawHash.slice(lastHashIndex + 1); return cleanHash === 'system' || cleanHash === 'normal' ? cleanHash : null; }, - syncConfigTypeFromHash(hash) { + async syncConfigTypeFromHash(hash) { const configType = this.extractConfigTypeFromHash(hash); if (!configType || configType === this.configType) { return false; } this.configType = configType; - this.onConfigTypeToggle(); + await this.onConfigTypeToggle(); return true; }, getConfigInfoList(abconf_id) { @@ -382,6 +448,7 @@ export default { for (let i = 0; i < this.configInfoList.length; i++) { if (this.configInfoList[i].id === abconf_id) { this.selectedConfigID = this.configInfoList[i].id; + this.currentConfigId = this.configInfoList[i].id; this.getConfig(abconf_id); matched = true; break; @@ -391,6 +458,7 @@ export default { if (!matched && this.configInfoList.length) { // 当找不到目标配置时,默认展示列表中的第一个配置 this.selectedConfigID = this.configInfoList[0].id; + this.currentConfigId = this.configInfoList[0].id; this.getConfig(this.selectedConfigID); } } @@ -418,6 +486,14 @@ export default { this.fetched = true this.metadata = res.data.data.metadata; this.configContentKey += 1; + // 获取配置后更新 + this.$nextTick(() => { + this.originalConfigData = JSON.parse(JSON.stringify(this.config_data)); + this.hasUnsavedChanges = false; + if (!this.isSystemConfig) { + this.currentConfigId = abconf_id || this.selectedConfigID; + } + }); }).catch((err) => { this.save_message = this.messages.loadError; this.save_message_snack = true; @@ -437,27 +513,37 @@ export default { postData.conf_id = this.selectedConfigID; } - axios.post('/api/config/astrbot/update', postData).then((res) => { + return axios.post('/api/config/astrbot/update', postData).then((res) => { if (res.data.status === "ok") { this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data); this.save_message = res.data.message || this.messages.saveSuccess; this.save_message_snack = true; this.save_message_success = "success"; + this.onConfigSaved(); if (this.isSystemConfig) { restartAstrBotRuntime(this.$refs.wfr).catch(() => {}) } + return { success: true }; } else { this.save_message = res.data.message || this.messages.saveError; this.save_message_snack = true; this.save_message_success = "error"; + return { success: false }; } }).catch((err) => { this.save_message = this.messages.saveError; this.save_message_snack = true; this.save_message_success = "error"; + return { success: false }; }); }, + // 重置未保存状态 + onConfigSaved() { + this.hasUnsavedChanges = false; + this.originalConfigData = JSON.parse(JSON.stringify(this.config_data)); + }, + configToString() { this.config_data_str = JSON.stringify(this.config_data, null, 2); this.config_data_has_changed = false; @@ -497,7 +583,7 @@ export default { this.save_message_success = "error"; }); }, - onConfigSelect(value) { + async onConfigSelect(value) { if (value === '_%manage%_') { this.configManageDialog = true; // 重置选择到之前的值 @@ -506,7 +592,44 @@ export default { this.getConfig(this.selectedConfigID); }); } else { - this.getConfig(value); + // 检查是否有未保存的更改 + if (this.hasUnsavedChanges) { + // 获取之前正在编辑的配置id + const prevConfigId = this.isSystemConfig ? 'default' : (this.currentConfigId || this.selectedConfigID || 'default'); + const message = this.tm('unsavedChangesWarning.switchConfig'); + const saveAndSwitch = await this.$refs.unsavedChangesDialog?.open({ + title: this.tm('unsavedChangesWarning.dialogTitle'), + message: message, + confirmHint: `${this.tm('unsavedChangesWarning.options.saveAndSwitch')}:${this.tm('unsavedChangesWarning.options.confirm')}`, + cancelHint: `${this.tm('unsavedChangesWarning.options.discardAndSwitch')}:${this.tm('unsavedChangesWarning.options.cancel')}`, + closeHint: `${this.tm('unsavedChangesWarning.options.closeCard')}:"x"` + }); + // 关闭弹窗不切换 + if (saveAndSwitch === 'close') { + return; + } + if (saveAndSwitch) { + // 设置临时变量保存切换后的id + const currentSelectedId = this.selectedConfigID; + // 把id设置回切换前的用于保存上一次的配置,保存完后恢复id为切换后的 + this.selectedConfigID = prevConfigId; + const result = await this.updateConfig(); + this.selectedConfigID = currentSelectedId; + if (result?.success) { + this.selectedConfigID = value; + this.getConfig(value); + } + return; + } else { + // 取消保存并切换配置 + this.selectedConfigID = value; + this.getConfig(value); + } + } else { + // 无未保存更改直接切换 + this.selectedConfigID = value; + this.getConfig(value); + } } }, startCreateConfig() { @@ -600,7 +723,34 @@ export default { this.save_message_success = "error"; }); }, - onConfigTypeToggle() { + async onConfigTypeToggle() { + // 检查是否有未保存的更改 + if (this.hasUnsavedChanges) { + const message = this.tm('unsavedChangesWarning.leavePage'); + const saveAndSwitch = await this.$refs.unsavedChangesDialog?.open({ + title: this.tm('unsavedChangesWarning.dialogTitle'), + message: message, + confirmHint: `${this.tm('unsavedChangesWarning.options.saveAndSwitch')}:${this.tm('unsavedChangesWarning.options.confirm')}`, + cancelHint: `${this.tm('unsavedChangesWarning.options.discardAndSwitch')}:${this.tm('unsavedChangesWarning.options.cancel')}`, + closeHint: `${this.tm('unsavedChangesWarning.options.closeCard')}:"x"` + }); + // 关闭弹窗 + if (saveAndSwitch === 'close') { + // 恢复路由 + const originalHash = this.isSystemConfig ? '#system' : '#normal'; + this.$router.replace('/config' + originalHash); + this.configType = this.isSystemConfig ? 'system' : 'normal'; + return; + } + if (saveAndSwitch) { + await this.updateConfig(); + // 系统配置保存后不跳转 + if (this.isSystemConfig) { + this.$router.replace('/config#system'); + return; + } + } + } this.isSystemConfig = this.configType === 'system'; this.fetched = false; // 重置加载状态