Feat/config leave confirm (#5249)
* feat: 配置文件增加未保存提示弹窗 * fix: 移除unsavedChangesDialog插件使用组件方式实现弹窗
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<v-dialog v-model="isOpen" max-width="480" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title d-flex align-center justify-space-between">
|
||||
<span>{{ title }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="handleClose"></v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="message-text">{{ message }}</div>
|
||||
<div class="action-hints">
|
||||
<span class="hint-item">{{ confirmHint }}</span>
|
||||
<span class="hint-item">{{ cancelHint }}</span>
|
||||
<span class="hint-item">{{ closeHint }}</span>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="gray" @click="handleCancel">{{ t('core.common.dialog.cancelButton') }}</v-btn>
|
||||
<v-btn color="red" @click="handleConfirm" class="confirm-button">{{ t('core.common.dialog.confirmButton') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const title = ref("");
|
||||
const message = ref("");
|
||||
const confirmHint = ref("");
|
||||
const cancelHint = ref("");
|
||||
const closeHint = ref("");
|
||||
let resolvePromise = null;
|
||||
|
||||
const open = (options) => {
|
||||
title.value = options.title || t('core.common.dialog.confirmTitle');
|
||||
message.value = options.message || t('core.common.dialog.confirmMessage');
|
||||
confirmHint.value = options.confirmHint || "";
|
||||
cancelHint.value = options.cancelHint || "";
|
||||
closeHint.value = options.closeHint || "";
|
||||
isOpen.value = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
isOpen.value = false;
|
||||
if (resolvePromise) resolvePromise(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
isOpen.value = false;
|
||||
if (resolvePromise) resolvePromise(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
isOpen.value = false;
|
||||
if (resolvePromise) resolvePromise('close');
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.message-text {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-hints {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.hint-item {
|
||||
color: var(--v-theme-secondaryText, #666);
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
color: rgb(239, 68, 68);
|
||||
}
|
||||
</style>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,5 +112,18 @@
|
||||
"addToConfig": "已加入配置",
|
||||
"fileCount": "文件:{count}",
|
||||
"done": "完成"
|
||||
},
|
||||
"unsavedChangesWarning": {
|
||||
"dialogTitle": "未保存的更改",
|
||||
"leavePage": "当前配置有未保存的更改,切换前是否保存?",
|
||||
"switchConfig": "切换配置文件会丢失当前未保存的更改,是否先保存?",
|
||||
"options": {
|
||||
"save": "保存",
|
||||
"saveAndSwitch": "保存并切换",
|
||||
"discardAndSwitch": "放弃更改并切换",
|
||||
"closeCard": "关闭弹窗",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="config-toolbar d-flex flex-row pr-4"
|
||||
style="margin-bottom: 16px; align-items: center; gap: 12px; width: 100%; justify-content: space-between;">
|
||||
<div class="config-toolbar-controls d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select class="config-select" style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
<v-select class="config-select" style="min-width: 130px;" :model-value="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
v-if="!isSystemConfig" item-value="id" :label="tm('configSelection.selectConfig')" hide-details density="compact" rounded="md"
|
||||
variant="outlined" @update:model-value="onConfigSelect">
|
||||
</v-select>
|
||||
@@ -191,6 +191,10 @@
|
||||
</div>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
|
||||
<!-- 未保存更改确认弹窗 -->
|
||||
<UnsavedChangesConfirmDialog ref="unsavedChangesDialog" />
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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; // 重置加载状态
|
||||
|
||||
|
||||
Reference in New Issue
Block a user