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 @@
+
+
+
+
+ {{ title }}
+
+
+
+ {{ message }}
+
+ {{ confirmHint }}
+ {{ cancelHint }}
+ {{ closeHint }}
+
+
+
+
+ {{ t('core.common.dialog.cancelButton') }}
+ {{ t('core.common.dialog.confirmButton') }}
+
+
+
+
+
+
+
+
+
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; // 重置加载状态