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/server-monitor.ts
|
||||||
web/apps/web-ele/src/api/core/wecom-sync.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/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/approval-center/
|
||||||
web/apps/web-ele/src/views/_core/data-source/
|
web/apps/web-ele/src/views/_core/data-source/
|
||||||
web/apps/web-ele/src/views/_core/database-connection/
|
web/apps/web-ele/src/views/_core/database-connection/
|
||||||
|
|||||||
@@ -6758,6 +6758,51 @@
|
|||||||
"sys_dept_id": null
|
"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",
|
"model": "core.menu.model.Menu",
|
||||||
"pk": "4CW_HY32ozaVOQpSz7njE",
|
"pk": "4CW_HY32ozaVOQpSz7njE",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
|||||||
'../views/_core/agent-chat/**/*.vue',
|
'../views/_core/agent-chat/**/*.vue',
|
||||||
'../views/_core/announcement/index.vue',
|
'../views/_core/announcement/index.vue',
|
||||||
'../views/_core/announcement/list.vue',
|
'../views/_core/announcement/list.vue',
|
||||||
|
'../views/_core/application/index.vue',
|
||||||
'../views/_core/authentication/login.vue',
|
'../views/_core/authentication/login.vue',
|
||||||
'../views/_core/authentication/oauth-callback.vue',
|
'../views/_core/authentication/oauth-callback.vue',
|
||||||
'../views/_core/chat/index.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