Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/bots/modal.vue
陈子默 b191d1aaed feat: 统一管理端弹窗与内容区交互样式
- 收敛管理端公共 Modal 链路,新增表单弹窗与普通内容弹窗包装\n- 迁移 Bot、知识库、插件、工作流、资源、MCP、数据中枢与系统管理页面级弹窗\n- 统一内容区工具栏、列表容器、导航与顶部按钮的视觉密度和交互节奏
2026-03-06 19:58:26 +08:00

351 lines
8.8 KiB
Vue

<script setup lang="ts">
import type { BotInfo } from '@easyflow/types';
import type { SaveBotParams, UpdateBotParams } from '#/api/ai/bot';
import { computed, nextTick, ref } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { $t } from '@easyflow/locales';
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
import { tryit } from 'radash';
import { saveBot, updateBotApi } from '#/api/ai/bot';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
const emit = defineEmits(['success']);
const createInitialFormData = (): SaveBotParams => ({
alias: '',
categoryId: '',
description: '',
icon: '',
status: 1,
title: '',
});
const dialogVisible = ref(false);
const dialogType = ref<'create' | 'edit'>('create');
const formRef = ref<InstanceType<typeof ElForm>>();
const formData = ref<SaveBotParams | UpdateBotParams>(createInitialFormData());
const loading = ref(false);
const rules = {
alias: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
};
const dialogTitle = computed(() => {
return `${$t(`button.${dialogType.value}`)}${$t('bot.chatAssistant')}`;
});
const dialogDescription = computed(() => {
return dialogType.value === 'create'
? $t('bot.modal.createDescription')
: $t('bot.modal.editDescription');
});
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) {
return;
}
loading.value = true;
const [err, res] = await tryit(
dialogType.value === 'create' ? saveBot : updateBotApi,
)(formData.value as SaveBotParams & UpdateBotParams);
if (!err && res.errorCode === 0) {
emit('success');
ElMessage.success(res.message || $t('message.saveOkMessage'));
dialogVisible.value = false;
loading.value = false;
return;
}
ElMessage.error(res?.message || $t('message.saveFail'));
loading.value = false;
}
async function open(type: 'create' | 'edit', bot?: BotInfo) {
formData.value = bot
? {
...createInitialFormData(),
alias: bot.alias,
categoryId: bot.categoryId,
description: bot.description,
icon: bot.icon,
id: bot.id,
status: bot.status,
title: bot.title,
}
: createInitialFormData();
dialogType.value = type;
dialogVisible.value = true;
await nextTick();
formRef.value?.clearValidate();
}
defineExpose({
open,
});
</script>
<template>
<EasyFlowFormModal
v-model:open="dialogVisible"
:confirm-loading="loading"
:confirm-text="$t('button.save')"
:description="dialogDescription"
:submitting="loading"
:title="dialogTitle"
width="lg"
@confirm="handleSubmit"
>
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
class="bot-modal-form"
label-position="top"
status-icon
>
<section class="bot-modal-section">
<header class="bot-modal-section__header">
<div>
<h4 class="bot-modal-section__title">
{{ $t('bot.modal.sectionAppearanceTitle') }}
</h4>
<p class="bot-modal-section__description">
{{ $t('bot.modal.sectionAppearanceDescription') }}
</p>
</div>
</header>
<div class="bot-modal-appearance-grid">
<ElFormItem
class="bot-modal-avatar-field"
:label="$t('common.avatar')"
prop="icon"
>
<UploadAvatar
v-model="formData.icon"
:size="88"
theme="soft-panel"
/>
</ElFormItem>
<div class="bot-modal-appearance-fields">
<ElFormItem :label="$t('aiWorkflow.title')" prop="title">
<ElInput v-model="formData.title" />
</ElFormItem>
<ElFormItem :label="$t('aiWorkflow.categoryId')" prop="categoryId">
<DictSelect
v-model="formData.categoryId"
dict-code="aiBotCategory"
/>
</ElFormItem>
<ElFormItem
class="bot-modal-grid__full"
:label="$t('plugin.alias')"
prop="alias"
>
<ElInput v-model="formData.alias" />
<div class="bot-modal-field-tip">
{{ $t('bot.modal.aliasHint') }}
</div>
</ElFormItem>
</div>
</div>
</section>
<section class="bot-modal-section bot-modal-section--compact">
<header class="bot-modal-section__header">
<div>
<h4 class="bot-modal-section__title">
{{ $t('bot.modal.sectionPublishTitle') }}
</h4>
<p class="bot-modal-section__description">
{{ $t('bot.modal.sectionPublishDescription') }}
</p>
</div>
</header>
<div class="bot-modal-publish-grid">
<ElFormItem :label="$t('plugin.description')" prop="description">
<ElInput v-model="formData.description" :rows="3" type="textarea" />
<div class="bot-modal-field-tip">
{{ $t('bot.modal.descriptionHint') }}
</div>
</ElFormItem>
<ElFormItem
class="bot-modal-publish-field"
:label="$t('aiWorkflow.status')"
prop="status"
>
<DictSelect v-model="formData.status" dict-code="showOrNot" />
<div class="bot-modal-field-tip">
{{ $t('bot.modal.statusHint') }}
</div>
</ElFormItem>
</div>
</section>
</ElForm>
</EasyFlowFormModal>
</template>
<style scoped>
.bot-modal-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.bot-modal-section {
border-radius: 14px;
border: 1px solid hsl(var(--modal-divider));
background: hsl(var(--modal-content-surface-strong));
box-shadow: none;
padding: 14px;
}
.bot-modal-section--compact {
padding-bottom: 12px;
}
.bot-modal-section__header {
margin-bottom: 10px;
}
.bot-modal-section__title {
margin: 0;
color: hsl(var(--text-strong));
font-size: 14px;
font-weight: 700;
line-height: 1.4;
}
.bot-modal-section__description {
margin: 3px 0 0;
color: hsl(var(--text-muted));
font-size: 11px;
line-height: 1.45;
}
.bot-modal-appearance-grid {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
gap: 14px 16px;
align-items: start;
}
.bot-modal-appearance-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 16px;
}
.bot-modal-grid__full {
grid-column: 1 / -1;
}
.bot-modal-publish-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 220px);
gap: 14px 16px;
align-items: start;
}
.bot-modal-publish-field {
margin-bottom: 0;
}
.bot-modal-avatar-field {
align-items: flex-start;
margin-bottom: 0;
}
.bot-modal-avatar-field :deep(.el-form-item__content) {
display: block;
}
.bot-modal-field-tip {
margin-top: 4px;
color: hsl(var(--text-muted));
font-size: 11px;
line-height: 1.5;
}
.bot-modal-form :deep(.el-form-item) {
margin-bottom: 0;
}
.bot-modal-form :deep(.el-form-item__label) {
padding-bottom: 6px;
color: hsl(var(--text-strong));
font-size: 12px;
font-weight: 600;
line-height: 1.4;
}
.bot-modal-form :deep(.el-input__wrapper),
.bot-modal-form :deep(.el-select__wrapper),
.bot-modal-form :deep(.el-textarea__inner) {
border-radius: 12px;
background: hsl(var(--input-background));
box-shadow: inset 0 0 0 1px hsl(var(--input) / 0.92);
transition:
box-shadow 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease;
}
.bot-modal-form :deep(.el-input__wrapper),
.bot-modal-form :deep(.el-select__wrapper) {
min-height: 42px;
}
.bot-modal-form :deep(.el-input__wrapper:hover),
.bot-modal-form :deep(.el-select__wrapper:hover),
.bot-modal-form :deep(.el-textarea__inner:hover) {
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.18);
}
.bot-modal-form :deep(.el-input__wrapper.is-focus),
.bot-modal-form :deep(.el-select__wrapper.is-focused),
.bot-modal-form :deep(.el-textarea__inner:focus) {
box-shadow:
inset 0 0 0 1px hsl(var(--primary) / 0.72),
0 0 0 4px hsl(var(--primary) / 0.12);
}
.bot-modal-form :deep(.el-textarea__inner) {
min-height: 88px;
padding: 10px 12px;
}
.bot-modal-form :deep(.el-form-item.is-error .el-input__wrapper),
.bot-modal-form :deep(.el-form-item.is-error .el-select__wrapper),
.bot-modal-form :deep(.el-form-item.is-error .el-textarea__inner) {
box-shadow:
inset 0 0 0 1px hsl(var(--destructive) / 0.8),
0 0 0 4px hsl(var(--destructive) / 0.08);
}
@media (max-width: 768px) {
.bot-modal-appearance-grid,
.bot-modal-appearance-fields,
.bot-modal-publish-grid {
grid-template-columns: 1fr;
}
}
</style>