Restore lightweight application management
This commit is contained in:
@@ -248,7 +248,6 @@ web/apps/web-ele/src/api/core/scheduler.ts
|
||||
web/apps/web-ele/src/api/core/server-monitor.ts
|
||||
web/apps/web-ele/src/api/core/wecom-sync.ts
|
||||
web/apps/web-ele/src/router/routes/modules/wiki.ts
|
||||
web/apps/web-ele/src/views/_core/application/
|
||||
web/apps/web-ele/src/views/_core/approval-center/
|
||||
web/apps/web-ele/src/views/_core/data-source/
|
||||
web/apps/web-ele/src/views/_core/database-connection/
|
||||
|
||||
@@ -6758,6 +6758,51 @@
|
||||
"sys_dept_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.menu.model.Menu",
|
||||
"pk": "ai-agent-admin-application-management",
|
||||
"fields": {
|
||||
"application_id": null,
|
||||
"is_system": true,
|
||||
"parent_id": null,
|
||||
"name": "Application",
|
||||
"title": "menu-title.applicationManagement",
|
||||
"authCode": null,
|
||||
"path": "/application",
|
||||
"type": "menu",
|
||||
"component": "/_core/application/index",
|
||||
"redirect": null,
|
||||
"activePath": null,
|
||||
"query": null,
|
||||
"noBasicLayout": false,
|
||||
"icon": "lucide:app-window",
|
||||
"activeIcon": null,
|
||||
"order": 35,
|
||||
"hideInMenu": false,
|
||||
"hideChildrenInMenu": false,
|
||||
"hideInBreadcrumb": false,
|
||||
"hideInTab": false,
|
||||
"affixTab": false,
|
||||
"affixTabOrder": null,
|
||||
"keepAlive": false,
|
||||
"maxNumOfOpenTab": null,
|
||||
"fullPathKey": true,
|
||||
"link": null,
|
||||
"iframeSrc": null,
|
||||
"openInNewWindow": false,
|
||||
"badge": null,
|
||||
"badgeType": null,
|
||||
"badgeVariants": null,
|
||||
"id": "ai-agent-admin-application-management",
|
||||
"sort": 35,
|
||||
"is_deleted": false,
|
||||
"sys_create_datetime": "2026-06-10T00:00:00.000000",
|
||||
"sys_update_datetime": "2026-06-10T00:00:00.000000",
|
||||
"sys_creator_id": null,
|
||||
"sys_modifier_id": null,
|
||||
"sys_dept_id": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.menu.model.Menu",
|
||||
"pk": "4CW_HY32ozaVOQpSz7njE",
|
||||
|
||||
@@ -18,6 +18,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
'../views/_core/agent-chat/**/*.vue',
|
||||
'../views/_core/announcement/index.vue',
|
||||
'../views/_core/announcement/list.vue',
|
||||
'../views/_core/application/index.vue',
|
||||
'../views/_core/authentication/login.vue',
|
||||
'../views/_core/authentication/oauth-callback.vue',
|
||||
'../views/_core/chat/index.vue',
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
<script setup lang="ts">
|
||||
import type { ApplicationListItem } from '#/api/core/application';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
AppWindow,
|
||||
Copy,
|
||||
Edit,
|
||||
IconifyIcon,
|
||||
Play,
|
||||
Plus,
|
||||
Square,
|
||||
Trash2,
|
||||
} from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElEmpty,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPagination,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
deleteApplicationApi,
|
||||
disableApplicationApi,
|
||||
getApplicationListApi,
|
||||
publishApplicationApi,
|
||||
} from '#/api/core/application';
|
||||
import ZqDialog from '#/components/zq-dialog/zq-dialog.vue';
|
||||
|
||||
import ApplicationFormModal from './modules/application-form-modal.vue';
|
||||
|
||||
defineOptions({ name: 'ApplicationList' });
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 应用列表
|
||||
const applicationList = ref<ApplicationListItem[]>([]);
|
||||
|
||||
// 分页
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 弹窗状态
|
||||
const showFormModal = ref(false);
|
||||
const editingApp = ref<ApplicationListItem | null>(null);
|
||||
|
||||
// 发布/停用对话框状态
|
||||
const showPublishDialog = ref(false);
|
||||
const showDisableDialog = ref(false);
|
||||
const currentApp = ref<ApplicationListItem | null>(null);
|
||||
const appUrl = ref('');
|
||||
|
||||
// Element Plus Tag 类型
|
||||
type TagType =
|
||||
| 'danger'
|
||||
| 'info'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| undefined;
|
||||
|
||||
// 应用类型映射
|
||||
const appTypeMap: Record<string, { color: TagType; labelKey: string }> = {
|
||||
ai: { labelKey: 'application.appTypes.ai', color: 'info' },
|
||||
dashboard: { labelKey: 'application.appTypes.dashboard', color: 'success' },
|
||||
form: { labelKey: 'application.appTypes.form', color: 'primary' },
|
||||
mixed: { labelKey: 'application.appTypes.mixed', color: undefined },
|
||||
screen: { labelKey: 'application.appTypes.screen', color: 'danger' },
|
||||
workflow: { labelKey: 'application.appTypes.workflow', color: 'warning' },
|
||||
};
|
||||
|
||||
// 状态映射
|
||||
const statusMap: Record<string, { color: TagType; labelKey: string }> = {
|
||||
disabled: { labelKey: 'application.appStatus.disabled', color: 'danger' },
|
||||
draft: { labelKey: 'application.appStatus.draft', color: 'info' },
|
||||
published: { labelKey: 'application.appStatus.published', color: 'success' },
|
||||
};
|
||||
|
||||
// 加载应用列表
|
||||
const loadApplications = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getApplicationListApi({
|
||||
page: pagination.value.current,
|
||||
pageSize: pagination.value.pageSize,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
applicationList.value = res.items || [];
|
||||
pagination.value.total = res.total || 0;
|
||||
} catch (error) {
|
||||
console.error('Failed to load applications:', error);
|
||||
ElMessage.error($t('application.loadFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新应用
|
||||
const handleCreate = () => {
|
||||
editingApp.value = null;
|
||||
showFormModal.value = true;
|
||||
};
|
||||
|
||||
// 编辑应用
|
||||
const handleEdit = (app: ApplicationListItem) => {
|
||||
editingApp.value = app;
|
||||
showFormModal.value = true;
|
||||
};
|
||||
|
||||
// 进入应用(在新 tab 打开子应用)
|
||||
const handleEnter = (app: ApplicationListItem) => {
|
||||
const subAppUrl = `${window.location.origin}/app/${app.code}`;
|
||||
window.open(subAppUrl, '_blank');
|
||||
};
|
||||
|
||||
// 设置开发(跳转到表单管理)
|
||||
// 发布应用
|
||||
const handlePublish = (app: ApplicationListItem) => {
|
||||
currentApp.value = app;
|
||||
appUrl.value = `${window.location.origin}/app/${app.code}`;
|
||||
showPublishDialog.value = true;
|
||||
};
|
||||
|
||||
// 确认发布
|
||||
const confirmPublish = async () => {
|
||||
if (!currentApp.value) return;
|
||||
try {
|
||||
await publishApplicationApi(currentApp.value.id);
|
||||
const isReEnable = currentApp.value.status === 'disabled';
|
||||
ElMessage.success(
|
||||
isReEnable ? $t('application.enableSuccess') : $t('application.publishSuccess'),
|
||||
);
|
||||
showPublishDialog.value = false;
|
||||
loadApplications();
|
||||
} catch {
|
||||
const wasDisabled = currentApp.value?.status === 'disabled';
|
||||
ElMessage.error(
|
||||
wasDisabled ? $t('application.enableFailed') : $t('application.publishFailed'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 停用应用
|
||||
const handleDisable = (app: ApplicationListItem) => {
|
||||
currentApp.value = app;
|
||||
appUrl.value = `${window.location.origin}/app/${app.code}`;
|
||||
showDisableDialog.value = true;
|
||||
};
|
||||
|
||||
// 确认停用
|
||||
const confirmDisable = async () => {
|
||||
if (!currentApp.value) return;
|
||||
try {
|
||||
await disableApplicationApi(currentApp.value.id);
|
||||
ElMessage.success($t('application.disableSuccess'));
|
||||
showDisableDialog.value = false;
|
||||
loadApplications();
|
||||
} catch {
|
||||
ElMessage.error($t('application.disableFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除应用
|
||||
const handleDelete = async (app: ApplicationListItem) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('application.deleteConfirmMsg', { name: app.name }),
|
||||
$t('application.deleteConfirm'),
|
||||
{
|
||||
confirmButtonText: $t('application.confirm'),
|
||||
cancelButtonText: $t('application.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
await deleteApplicationApi(app.id);
|
||||
ElMessage.success($t('application.deleteSuccess'));
|
||||
loadApplications();
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.value.current = 1;
|
||||
loadApplications();
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.value.current = page;
|
||||
loadApplications();
|
||||
};
|
||||
|
||||
// 每页条数变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.value.pageSize = size;
|
||||
pagination.value.current = 1;
|
||||
loadApplications();
|
||||
};
|
||||
|
||||
// 保存成功回调
|
||||
const handleSaveSuccess = () => {
|
||||
loadApplications();
|
||||
};
|
||||
|
||||
// 复制链接
|
||||
const copyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(appUrl.value);
|
||||
ElMessage.success($t('application.copySuccess'));
|
||||
} catch {
|
||||
ElMessage.error($t('application.copyFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadApplications();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="application-list">
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<ElButton type="primary" @click="handleCreate">
|
||||
<Plus class="mr-1 h-4 w-4" />
|
||||
{{ $t('application.createApp') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElInput
|
||||
v-model="searchKeyword"
|
||||
:placeholder="$t('application.searchPlaceholder')"
|
||||
class="!w-64"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<ElButton type="primary" @click="handleSearch">
|
||||
{{ $t('application.search') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 应用列表 -->
|
||||
<div
|
||||
v-if="applicationList.length > 0"
|
||||
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
|
||||
>
|
||||
<ElCard
|
||||
v-for="app in applicationList"
|
||||
shadow="hover"
|
||||
:key="app.id"
|
||||
class="group application-card cursor-pointer transition-shadow"
|
||||
:body-style="{ padding: '0' }"
|
||||
style="border: none"
|
||||
@click="handleEnter(app)"
|
||||
>
|
||||
<div class="p-4">
|
||||
<!-- 头部:图标 + 右侧信息区 -->
|
||||
<div class="mb-4 flex gap-3">
|
||||
<div class="app-icon flex-shrink-0">
|
||||
<IconifyIcon
|
||||
v-if="app.icon"
|
||||
:icon="app.icon"
|
||||
class="h-5 w-5 text-white"
|
||||
/>
|
||||
<AppWindow v-else class="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<!-- name + 操作 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-0 flex-1 whitespace-nowrap text-sm font-medium group-hover:truncate">
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-shrink-0 items-center -space-x-1 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
@click.stop
|
||||
>
|
||||
<ElTooltip
|
||||
:content="$t('application.edit')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleEdit(app)">
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip
|
||||
v-if="app.status === 'draft' || app.status === 'disabled'"
|
||||
:content="
|
||||
app.status === 'disabled'
|
||||
? $t('application.enable')
|
||||
: $t('application.publish')
|
||||
"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handlePublish(app)">
|
||||
<Play class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip
|
||||
v-if="app.status === 'published'"
|
||||
:content="$t('application.disable')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleDisable(app)">
|
||||
<Square class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip
|
||||
:content="$t('application.delete')"
|
||||
placement="top"
|
||||
>
|
||||
<ElButton text size="small" @click="handleDelete(app)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- code -->
|
||||
<div class="text-muted-foreground font-mono text-xs">
|
||||
{{ app.code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div
|
||||
v-if="app.description"
|
||||
class="text-muted-foreground mb-4 line-clamp-1 text-xs"
|
||||
>
|
||||
{{ app.description }}
|
||||
</div>
|
||||
<!-- 第三行:标签 + 创建时间 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-1">
|
||||
<ElTag size="small" :type="appTypeMap[app.app_type]?.color">
|
||||
{{
|
||||
appTypeMap[app.app_type]?.labelKey
|
||||
? $t(appTypeMap[app.app_type]!.labelKey)
|
||||
: app.app_type
|
||||
}}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
size="small"
|
||||
:type="statusMap[app.status]?.color ?? 'info'"
|
||||
>
|
||||
{{
|
||||
statusMap[app.status]?.labelKey
|
||||
? $t(statusMap[app.status]!.labelKey)
|
||||
: app.status
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ app.sys_create_datetime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ElEmpty v-else :description="$t('application.noApps')" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<template #footer>
|
||||
<div class="flex w-full items-center justify-end">
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[12, 24, 36, 48]"
|
||||
:pager-count="7"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
size="small"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Page>
|
||||
|
||||
<!-- 创建/编辑弹窗 -->
|
||||
<ApplicationFormModal
|
||||
v-model="showFormModal"
|
||||
:application="editingApp"
|
||||
@success="handleSaveSuccess"
|
||||
/>
|
||||
|
||||
<!-- 发布对话框 -->
|
||||
<ZqDialog
|
||||
v-model="showPublishDialog"
|
||||
:title="
|
||||
currentApp?.status === 'disabled'
|
||||
? $t('application.enableApp')
|
||||
: $t('application.publishApp')
|
||||
"
|
||||
width="500px"
|
||||
:confirm-text="
|
||||
currentApp?.status === 'disabled'
|
||||
? $t('application.confirmEnable')
|
||||
: $t('application.confirmPublish')
|
||||
"
|
||||
@confirm="confirmPublish"
|
||||
@cancel="showPublishDialog = false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<div>
|
||||
<p class="mb-2 text-sm">
|
||||
{{
|
||||
currentApp?.status === 'disabled'
|
||||
? $t('application.enableConfirmMsg', {
|
||||
name: currentApp?.name,
|
||||
})
|
||||
: $t('application.publishConfirmMsg', {
|
||||
name: currentApp?.name,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{{
|
||||
currentApp?.status === 'disabled'
|
||||
? $t('application.enableSuccessMsg')
|
||||
: $t('application.publishSuccessMsg')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-secondary flex items-center justify-between rounded p-3">
|
||||
<span class="mr-2 flex-1 truncate text-sm">{{ appUrl }}</span>
|
||||
<ElButton text size="small" @click="copyLink">
|
||||
<Copy class="h-4 w-4" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
|
||||
<!-- 停用对话框 -->
|
||||
<ZqDialog
|
||||
v-model="showDisableDialog"
|
||||
:title="$t('application.disableApp')"
|
||||
width="500px"
|
||||
:confirm-text="$t('application.confirmDisable')"
|
||||
@confirm="confirmDisable"
|
||||
@cancel="showDisableDialog = false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<div>
|
||||
<p class="mb-2 text-sm">
|
||||
{{
|
||||
$t('application.disableConfirmMsg', { name: currentApp?.name })
|
||||
}}
|
||||
</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{{ $t('application.appLink') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-secondary flex items-center justify-between rounded p-3">
|
||||
<span class="mr-2 flex-1 truncate text-sm">{{ appUrl }}</span>
|
||||
<ElButton text size="small" @click="copyLink">
|
||||
<Copy class="h-4 w-4" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ZqDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.application-card :deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--el-color-primary-light-3),
|
||||
var(--el-color-primary)
|
||||
);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,345 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
ApplicationCreateInput,
|
||||
ApplicationListItem,
|
||||
ApplicationUpdateInput,
|
||||
AppType,
|
||||
} from '#/api/core/application';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElCol,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
checkApplicationUniqueApi,
|
||||
createApplicationApi,
|
||||
updateApplicationApi,
|
||||
} from '#/api/core/application';
|
||||
import ZqDialog from '#/components/zq-dialog/zq-dialog.vue';
|
||||
import { ZqIconPicker } from '#/components/zq-form/zq-icon-picker';
|
||||
import ZqMenuSelector from '#/components/zq-form/zq-menu-selector/zq-menu-selector.vue';
|
||||
|
||||
defineOptions({ name: 'ApplicationFormModal' });
|
||||
|
||||
const props = defineProps<{
|
||||
application?: ApplicationListItem | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>({ default: false });
|
||||
|
||||
// Dialog 引用
|
||||
const dialogRef = ref();
|
||||
|
||||
// 表单数据
|
||||
const formData = ref<{
|
||||
app_type: AppType;
|
||||
code: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
name: string;
|
||||
system_menu_ids: string[];
|
||||
}>({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
app_type: 'mixed',
|
||||
system_menu_ids: [],
|
||||
});
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref();
|
||||
|
||||
// 是否编辑模式
|
||||
const isEdit = computed(() => !!props.application);
|
||||
|
||||
// 弹窗标题
|
||||
const title = computed(() =>
|
||||
isEdit.value ? $t('application.editApp') : $t('application.createApp'),
|
||||
);
|
||||
|
||||
// 应用类型选项
|
||||
const appTypeOptions = computed<Array<{ label: string; value: AppType }>>(
|
||||
() => [
|
||||
{ label: $t('application.appTypes.mixed'), value: 'mixed' },
|
||||
// { label: $t('application.appTypes.form'), value: 'form' },
|
||||
{ label: $t('application.appTypes.workflow'), value: 'workflow' },
|
||||
{ label: $t('application.appTypes.ai'), value: 'ai' },
|
||||
// { label: $t('application.appTypes.dashboard'), value: 'dashboard' },
|
||||
// { label: $t('application.appTypes.screen'), value: 'screen' },
|
||||
],
|
||||
);
|
||||
|
||||
// 表单验证规则
|
||||
const rules = computed(() => ({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('application.validation.nameRequired'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
min: 2,
|
||||
max: 100,
|
||||
message: $t('application.validation.nameLength'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('application.validation.codeRequired'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
pattern: /^[a-z][\w-]*$/i,
|
||||
message: $t('application.validation.codePattern'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
min: 2,
|
||||
max: 100,
|
||||
message: $t('application.validation.codeLength'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
app_type: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('application.validation.typeRequired'),
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// 监听弹窗打开,初始化表单数据
|
||||
watch(visible, (val) => {
|
||||
if (val) {
|
||||
formData.value = props.application
|
||||
? {
|
||||
name: props.application.name,
|
||||
code: props.application.code,
|
||||
description: props.application.description || '',
|
||||
icon: props.application.icon || '',
|
||||
app_type: props.application.app_type as AppType,
|
||||
system_menu_ids: (props.application as any).system_menu_ids || [],
|
||||
}
|
||||
: {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
app_type: 'mixed',
|
||||
system_menu_ids: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 检查编码唯一性
|
||||
async function checkCodeUnique(code: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await checkApplicationUniqueApi(
|
||||
'code',
|
||||
code,
|
||||
props.application?.id,
|
||||
);
|
||||
return res.data?.unique ?? true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查名称唯一性
|
||||
async function checkNameUnique(name: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await checkApplicationUniqueApi(
|
||||
'name',
|
||||
name,
|
||||
props.application?.id,
|
||||
);
|
||||
return res.data?.unique ?? true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
dialogRef.value?.setConfirmLoading(true);
|
||||
try {
|
||||
// 检查编码唯一性
|
||||
const codeUnique = await checkCodeUnique(formData.value.code);
|
||||
if (!codeUnique) {
|
||||
ElMessage.error('应用编码已存在');
|
||||
dialogRef.value?.setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查名称唯一性
|
||||
const nameUnique = await checkNameUnique(formData.value.name);
|
||||
if (!nameUnique) {
|
||||
ElMessage.error('应用名称已存在');
|
||||
dialogRef.value?.setConfirmLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEdit.value && props.application) {
|
||||
const updateData: ApplicationUpdateInput = {
|
||||
name: formData.value.name,
|
||||
code: formData.value.code,
|
||||
description: formData.value.description,
|
||||
icon: formData.value.icon,
|
||||
app_type: formData.value.app_type,
|
||||
system_menu_ids: formData.value.system_menu_ids,
|
||||
};
|
||||
await updateApplicationApi(props.application.id, updateData);
|
||||
ElMessage.success('更新成功');
|
||||
} else {
|
||||
const createData: ApplicationCreateInput = {
|
||||
name: formData.value.name,
|
||||
code: formData.value.code,
|
||||
description: formData.value.description,
|
||||
icon: formData.value.icon,
|
||||
app_type: formData.value.app_type,
|
||||
system_menu_ids: formData.value.system_menu_ids,
|
||||
};
|
||||
await createApplicationApi(createData);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} catch {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||
} finally {
|
||||
dialogRef.value?.setConfirmLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
function handleCancel() {
|
||||
formRef.value?.resetFields();
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
ref="dialogRef"
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="640px"
|
||||
:confirm-text="isEdit ? $t('application.save') : $t('application.create')"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('application.appName')" prop="name">
|
||||
<ElInput
|
||||
v-model="formData.name"
|
||||
:placeholder="$t('application.appNamePlaceholder')"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('application.appCode')" prop="code">
|
||||
<ElInput
|
||||
v-model="formData.code"
|
||||
:placeholder="$t('application.appCodePlaceholder')"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('application.appType')" prop="app_type">
|
||||
<ElSelect
|
||||
v-model="formData.app_type"
|
||||
:placeholder="$t('application.appTypePlaceholder')"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="opt in appTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem
|
||||
:label="$t('application.appDescription')"
|
||||
prop="description"
|
||||
>
|
||||
<ElInput
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:placeholder="$t('application.appDescriptionPlaceholder')"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('application.appIcon')" prop="icon">
|
||||
<ZqIconPicker
|
||||
v-model="formData.icon"
|
||||
prefix="lucide"
|
||||
:auto-fetch-api="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem
|
||||
:label="$t('application.systemMenu')"
|
||||
prop="system_menu_ids"
|
||||
>
|
||||
<ZqMenuSelector
|
||||
v-model="formData.system_menu_ids"
|
||||
:multiple="true"
|
||||
:system-only="true"
|
||||
:placeholder="$t('application.systemMenuPlaceholder')"
|
||||
:dialog-title="$t('application.selectSystemMenu')"
|
||||
dialog-width="500px"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user