feat: add naive infra
This commit is contained in:
270
apps/web-naive/src/views/infra/apiAccessLog/data.ts
Normal file
270
apps/web-naive/src/views/infra/apiAccessLog/data.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { JsonViewer } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择用户类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'applicationName',
|
||||
label: '应用名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入应用名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'beginTime',
|
||||
label: '请求时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'duration',
|
||||
label: '执行时长',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入执行时长',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'resultCode',
|
||||
label: '结果码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入结果码',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
title: '用户类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.USER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
title: '应用名',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
title: '请求方法',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'requestUrl',
|
||||
title: '请求地址',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'beginTime',
|
||||
title: '请求时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
title: '执行时长',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) => `${cellValue} ms`,
|
||||
},
|
||||
{
|
||||
field: 'resultCode',
|
||||
title: '操作结果',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => {
|
||||
return row.resultCode === 0 ? '成功' : `失败(${row.resultMsg})`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operateModule',
|
||||
title: '操作模块',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'operateName',
|
||||
title: '操作名',
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'operateType',
|
||||
title: '操作类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_OPERATE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
label: '日志编号',
|
||||
},
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
label: '应用名',
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
label: '用户Id',
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userIp',
|
||||
label: '用户 IP',
|
||||
},
|
||||
{
|
||||
field: 'userAgent',
|
||||
label: '用户 UA',
|
||||
},
|
||||
{
|
||||
label: '请求信息',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data.requestParams) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(data.requestParams),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'responseBody',
|
||||
label: '请求结果',
|
||||
},
|
||||
{
|
||||
label: '请求时间',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
return `${formatDateTime(data.beginTime)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请求耗时',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
return data?.duration ? `${data.duration} ms` : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '操作结果',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.resultCode === 0) {
|
||||
return '正常';
|
||||
} else if (data && data.resultCode > 0) {
|
||||
return `失败 | ${data.resultCode} | ${data.resultMsg}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operateModule',
|
||||
label: '操作模块',
|
||||
},
|
||||
{
|
||||
field: 'operateName',
|
||||
label: '操作名',
|
||||
},
|
||||
{
|
||||
field: 'operateType',
|
||||
label: '操作类型',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_OPERATE_TYPE,
|
||||
value: data?.operateType,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
107
apps/web-naive/src/views/infra/apiAccessLog/index.vue
Normal file
107
apps/web-naive/src/views/infra/apiAccessLog/index.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
exportApiAccessLog,
|
||||
getApiAccessLogPage,
|
||||
} from '#/api/infra/api-access-log';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportApiAccessLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: 'API 访问日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看 API 访问日志详情 */
|
||||
function handleDetail(row: InfraApiAccessLogApi.ApiAccessLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getApiAccessLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraApiAccessLogApi.ApiAccessLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="handleRefresh" />
|
||||
<Grid table-title="API 访问日志列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:api-access-log:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:api-access-log:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="API 访问日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
251
apps/web-naive/src/views/infra/apiErrorLog/data.ts
Normal file
251
apps/web-naive/src/views/infra/apiErrorLog/data.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { JsonViewer } from '@vben/common-ui';
|
||||
import { DICT_TYPE, InfraApiErrorLogProcessStatusEnum } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择用户类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'applicationName',
|
||||
label: '应用名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入应用名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'exceptionTime',
|
||||
label: '异常时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'processStatus',
|
||||
label: '处理状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||
'number',
|
||||
),
|
||||
allowClear: true,
|
||||
placeholder: '请选择处理状态',
|
||||
},
|
||||
defaultValue: InfraApiErrorLogProcessStatusEnum.INIT,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
title: '用户类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.USER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
title: '应用名',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
title: '请求方法',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'requestUrl',
|
||||
title: '请求地址',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'exceptionTime',
|
||||
title: '异常发生时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'exceptionName',
|
||||
title: '异常名',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'processStatus',
|
||||
title: '处理状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
minWidth: 220,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
label: '日志编号',
|
||||
},
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
},
|
||||
{
|
||||
field: 'applicationName',
|
||||
label: '应用名',
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
label: '用户Id',
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userIp',
|
||||
label: '用户 IP',
|
||||
},
|
||||
{
|
||||
field: 'userAgent',
|
||||
label: '用户 UA',
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data.requestParams) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(data.requestParams),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'exceptionTime',
|
||||
label: '异常时间',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return formatDateTime(data?.exceptionTime || '') as string;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'exceptionName',
|
||||
label: '异常名',
|
||||
},
|
||||
{
|
||||
field: 'exceptionStackTrace',
|
||||
label: '异常堆栈',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) =>
|
||||
!data?.exceptionStackTrace,
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data?.exceptionStackTrace) {
|
||||
return h('textarea', {
|
||||
value: data.exceptionStackTrace,
|
||||
style:
|
||||
'width: 100%; min-height: 200px; max-height: 400px; resize: vertical;',
|
||||
readonly: true,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'processStatus',
|
||||
label: '处理状态',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||
value: data?.processStatus,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'processUserId',
|
||||
label: '处理人',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) => !data?.processUserId,
|
||||
},
|
||||
{
|
||||
field: 'processTime',
|
||||
label: '处理时间',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) => !data?.processTime,
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return formatDateTime(data?.processTime || '') as string;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
155
apps/web-naive/src/views/infra/apiErrorLog/index.vue
Normal file
155
apps/web-naive/src/views/infra/apiErrorLog/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { InfraApiErrorLogProcessStatusEnum } from '@vben/constants';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
exportApiErrorLog,
|
||||
getApiErrorLogPage,
|
||||
updateApiErrorLogStatus,
|
||||
} from '#/api/infra/api-error-log';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportApiErrorLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: 'API 错误日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看 API 错误日志详情 */
|
||||
function handleDetail(row: InfraApiErrorLogApi.ApiErrorLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 处理已处理 / 已忽略的操作 */
|
||||
async function handleProcess(id: number, processStatus: number) {
|
||||
await confirm({
|
||||
content: `确认标记为${InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'}?`,
|
||||
});
|
||||
const hideLoading = message.loading('正在处理中...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await updateApiErrorLogStatus(id, processStatus);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getApiErrorLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraApiErrorLogApi.ApiErrorLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="handleRefresh" />
|
||||
<Grid table-title="API 错误日志列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:api-error-log:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:api-error-log:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '已处理',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:api-error-log:update-status'],
|
||||
ifShow:
|
||||
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT,
|
||||
onClick: handleProcess.bind(
|
||||
null,
|
||||
row.id,
|
||||
InfraApiErrorLogProcessStatusEnum.DONE,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '已忽略',
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:api-error-log:update-status'],
|
||||
ifShow:
|
||||
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT,
|
||||
onClick: handleProcess.bind(
|
||||
null,
|
||||
row.id,
|
||||
InfraApiErrorLogProcessStatusEnum.IGNORE,
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="API 错误日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
9
apps/web-naive/src/views/infra/build/index.vue
Normal file
9
apps/web-naive/src/views/infra/build/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'InfraBuild' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page> 待完成 </Page>
|
||||
</template>
|
||||
557
apps/web-naive/src/views/infra/codegen/data.ts
Normal file
557
apps/web-naive/src/views/infra/codegen/data.ts
Normal file
@@ -0,0 +1,557 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { getDataSourceConfigList } from '#/api/infra/data-source-config';
|
||||
import { getMenuList } from '#/api/system/menu';
|
||||
import { $t } from '#/locales';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 导入数据库表的表单 */
|
||||
export function useImportTableFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'dataSourceConfigId',
|
||||
label: '数据源',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getDataSourceConfigList();
|
||||
return data.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
autoSelect: 'first',
|
||||
placeholder: '请选择数据源',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '表名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入表名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'comment',
|
||||
label: '表描述',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入表描述',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 导入数据库表表格列定义 */
|
||||
export function useImportTableColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{ field: 'name', title: '表名称', minWidth: 200 },
|
||||
{ field: 'comment', title: '表描述', minWidth: 200 },
|
||||
];
|
||||
}
|
||||
|
||||
/** 基本信息表单的 schema */
|
||||
export function useBasicInfoFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'tableName',
|
||||
label: '表名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入仓库名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'tableComment',
|
||||
label: '表描述',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入表描述',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'className',
|
||||
label: '实体类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入实体类名称',
|
||||
},
|
||||
rules: 'required',
|
||||
help: '默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。',
|
||||
},
|
||||
{
|
||||
fieldName: 'author',
|
||||
label: '作者',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入作者',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 3,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 生成信息表单基础 schema */
|
||||
export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'templateType',
|
||||
label: '生成模板',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE,
|
||||
'number',
|
||||
),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'frontType',
|
||||
label: '前端类型',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE, 'number'),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'scene',
|
||||
label: '生成场景',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE, 'number'),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'parentMenuId',
|
||||
label: '上级菜单',
|
||||
help: '分配到指定菜单下,例如 系统管理',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: async () => {
|
||||
const data = await getMenuList();
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级菜单',
|
||||
} as SystemMenuApi.Menu);
|
||||
return handleTree(data);
|
||||
},
|
||||
class: 'w-full',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级菜单',
|
||||
filterTreeNode(input: string, node: Recordable<any>) {
|
||||
if (!input || input.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const name: string = node.label ?? '';
|
||||
if (!name) return false;
|
||||
return name.includes(input) || $t(name).includes(input);
|
||||
},
|
||||
showSearch: true,
|
||||
treeDefaultExpandedKeys: [0],
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
title({ label, icon }: { icon: string; label: string }) {
|
||||
const components = [];
|
||||
if (!label) return '';
|
||||
if (icon) {
|
||||
components.push(h(IconifyIcon, { class: 'size-4', icon }));
|
||||
}
|
||||
components.push(h('span', { class: '' }, $t(label || '')));
|
||||
return h('div', { class: 'flex items-center gap-1' }, components);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'moduleName',
|
||||
label: '模块名',
|
||||
help: '模块名,即一级目录,例如 system、infra、tool 等等',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'businessName',
|
||||
label: '业务名',
|
||||
help: '业务名,即二级目录,例如 user、permission、dict 等等',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'className',
|
||||
label: '类名称',
|
||||
help: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'classComment',
|
||||
label: '类描述',
|
||||
help: '用作类描述,例如 用户',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 树表信息 schema */
|
||||
export function useGenerationInfoTreeFormSchema(
|
||||
columns: InfraCodegenApi.CodegenColumn[] = [],
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: 'treeDivider',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['树表信息'],
|
||||
};
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'treeParentColumnId',
|
||||
label: '父编号字段',
|
||||
help: '树显示的父编码字段名,例如 parent_Id',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
placeholder: '请选择',
|
||||
options: columns.map((column) => ({
|
||||
label: column.columnName,
|
||||
value: column.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'treeNameColumnId',
|
||||
label: '名称字段',
|
||||
help: '树节点显示的名称字段,一般是 name',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
placeholder: '请选择名称字段',
|
||||
options: columns.map((column) => ({
|
||||
label: column.columnName,
|
||||
value: column.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 主子表信息 schema */
|
||||
export function useGenerationInfoSubTableFormSchema(
|
||||
columns: InfraCodegenApi.CodegenColumn[] = [],
|
||||
tables: InfraCodegenApi.CodegenTable[] = [],
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: 'subDivider',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['主子表信息'],
|
||||
};
|
||||
},
|
||||
formItemClass: 'md:col-span-2',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'masterTableId',
|
||||
label: '关联的主表',
|
||||
help: '关联主表(父表)的表名, 如:system_user',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
placeholder: '请选择',
|
||||
options: tables.map((table) => ({
|
||||
label: `${table.tableName}:${table.tableComment}`,
|
||||
value: table.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'subJoinColumnId',
|
||||
label: '子表关联的字段',
|
||||
help: '子表关联的字段, 如:user_id',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
placeholder: '请选择',
|
||||
options: columns.map((column) => ({
|
||||
label: `${column.columnName}:${column.columnComment}`,
|
||||
value: column.id,
|
||||
})),
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'subJoinMany',
|
||||
label: '关联关系',
|
||||
help: '主表与子表的关联关系',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
placeholder: '请选择',
|
||||
options: [
|
||||
{
|
||||
label: '一对多',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: '一对一',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'tableName',
|
||||
label: '表名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入表名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'tableComment',
|
||||
label: '表描述',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入表描述',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
getDataSourceConfigName?: (dataSourceConfigId: number) => string | undefined,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'dataSourceConfigId',
|
||||
title: '数据源',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) => getDataSourceConfigName?.(cellValue) || '-',
|
||||
},
|
||||
{
|
||||
field: 'tableName',
|
||||
title: '表名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'tableComment',
|
||||
title: '表描述',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'className',
|
||||
title: '实体',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 代码生成表格列定义 */
|
||||
export function useCodegenColumnTableColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ field: 'columnName', title: '字段列名', minWidth: 130 },
|
||||
{
|
||||
field: 'columnComment',
|
||||
title: '字段描述',
|
||||
minWidth: 100,
|
||||
slots: { default: 'columnComment' },
|
||||
},
|
||||
{ field: 'dataType', title: '物理类型', minWidth: 100 },
|
||||
{
|
||||
field: 'javaType',
|
||||
title: 'Java 类型',
|
||||
minWidth: 130,
|
||||
slots: { default: 'javaType' },
|
||||
params: {
|
||||
options: [
|
||||
{ label: 'Long', value: 'Long' },
|
||||
{ label: 'String', value: 'String' },
|
||||
{ label: 'Integer', value: 'Integer' },
|
||||
{ label: 'Double', value: 'Double' },
|
||||
{ label: 'BigDecimal', value: 'BigDecimal' },
|
||||
{ label: 'LocalDateTime', value: 'LocalDateTime' },
|
||||
{ label: 'Boolean', value: 'Boolean' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'javaField',
|
||||
title: 'Java 属性',
|
||||
minWidth: 100,
|
||||
slots: { default: 'javaField' },
|
||||
},
|
||||
{
|
||||
field: 'createOperation',
|
||||
title: '插入',
|
||||
width: 40,
|
||||
slots: { default: 'createOperation' },
|
||||
},
|
||||
{
|
||||
field: 'updateOperation',
|
||||
title: '编辑',
|
||||
width: 40,
|
||||
slots: { default: 'updateOperation' },
|
||||
},
|
||||
{
|
||||
field: 'listOperationResult',
|
||||
title: '列表',
|
||||
width: 40,
|
||||
slots: { default: 'listOperationResult' },
|
||||
},
|
||||
{
|
||||
field: 'listOperation',
|
||||
title: '查询',
|
||||
width: 40,
|
||||
slots: { default: 'listOperation' },
|
||||
},
|
||||
{
|
||||
field: 'listOperationCondition',
|
||||
title: '查询方式',
|
||||
minWidth: 100,
|
||||
slots: { default: 'listOperationCondition' },
|
||||
params: {
|
||||
options: [
|
||||
{ label: '=', value: '=' },
|
||||
{ label: '!=', value: '!=' },
|
||||
{ label: '>', value: '>' },
|
||||
{ label: '>=', value: '>=' },
|
||||
{ label: '<', value: '<' },
|
||||
{ label: '<=', value: '<=' },
|
||||
{ label: 'LIKE', value: 'LIKE' },
|
||||
{ label: 'BETWEEN', value: 'BETWEEN' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nullable',
|
||||
title: '允许空',
|
||||
width: 60,
|
||||
slots: { default: 'nullable' },
|
||||
},
|
||||
{
|
||||
field: 'htmlType',
|
||||
title: '显示类型',
|
||||
width: 130,
|
||||
slots: { default: 'htmlType' },
|
||||
params: {
|
||||
options: [
|
||||
{ label: '文本框', value: 'input' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '下拉框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '复选框', value: 'checkbox' },
|
||||
{ label: '日期控件', value: 'datetime' },
|
||||
{ label: '图片上传', value: 'imageUpload' },
|
||||
{ label: '文件上传', value: 'fileUpload' },
|
||||
{ label: '富文本控件', value: 'editor' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'dictType',
|
||||
title: '字典类型',
|
||||
width: 120,
|
||||
slots: { default: 'dictType' },
|
||||
},
|
||||
{
|
||||
field: 'example',
|
||||
title: '示例',
|
||||
minWidth: 100,
|
||||
slots: { default: 'example' },
|
||||
},
|
||||
];
|
||||
}
|
||||
164
apps/web-naive/src/views/infra/codegen/edit/index.vue
Normal file
164
apps/web-naive/src/views/infra/codegen/edit/index.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { ref, unref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { NButton, NStep, NSteps } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getCodegenTable, updateCodegenTable } from '#/api/infra/codegen';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import BasicInfo from '../modules/basic-info.vue';
|
||||
import ColumnInfo from '../modules/column-info.vue';
|
||||
import GenerationInfo from '../modules/generation-info.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const currentStep = ref(1);
|
||||
const formData = ref<InfraCodegenApi.CodegenDetail>({
|
||||
table: {} as InfraCodegenApi.CodegenTable,
|
||||
columns: [],
|
||||
});
|
||||
|
||||
/** 表单引用 */
|
||||
const basicInfoRef = ref<InstanceType<typeof BasicInfo>>();
|
||||
const columnInfoRef = ref<InstanceType<typeof ColumnInfo>>();
|
||||
const generateInfoRef = ref<InstanceType<typeof GenerationInfo>>();
|
||||
|
||||
/** 获取详情数据 */
|
||||
async function getDetail() {
|
||||
const id = route.query.id as any;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
formData.value = await getCodegenTable(id);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function submitForm() {
|
||||
// 表单验证
|
||||
const basicInfoValid = await basicInfoRef.value?.validate();
|
||||
if (!basicInfoValid) {
|
||||
message.warning('保存失败,原因:基本信息表单校验失败请检查!!!');
|
||||
return;
|
||||
}
|
||||
const generateInfoValid = await generateInfoRef.value?.validate();
|
||||
if (!generateInfoValid) {
|
||||
message.warning('保存失败,原因:生成信息表单校验失败请检查!!!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const hideLoading = message.loading($t('ui.actionMessage.updating'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
// 拼接相关信息
|
||||
const basicInfo = await basicInfoRef.value?.getValues();
|
||||
const columns = columnInfoRef.value?.getData() || unref(formData).columns;
|
||||
const generateInfo = await generateInfoRef.value?.getValues();
|
||||
await updateCodegenTable({
|
||||
table: { ...unref(formData).table, ...basicInfo, ...generateInfo },
|
||||
columns,
|
||||
});
|
||||
// 关闭并提示
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
close();
|
||||
} catch (error) {
|
||||
console.error('保存失败', error);
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回列表 */
|
||||
const tabs = useTabs();
|
||||
function close() {
|
||||
tabs.closeCurrentTab();
|
||||
router.push('/infra/codegen');
|
||||
}
|
||||
|
||||
/** 下一步 */
|
||||
function nextStep() {
|
||||
currentStep.value += 1;
|
||||
}
|
||||
|
||||
/** 上一步 */
|
||||
function prevStep() {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 步骤配置 */
|
||||
const steps = [
|
||||
{
|
||||
title: '基本信息',
|
||||
key: 1,
|
||||
},
|
||||
{
|
||||
title: '字段信息',
|
||||
key: 2,
|
||||
},
|
||||
{
|
||||
title: '生成信息',
|
||||
key: 3,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化
|
||||
getDetail();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<div class="bg-card flex h-[95%] flex-col rounded-md p-4">
|
||||
<NSteps :current="currentStep" class="mb-8 rounded shadow-sm">
|
||||
<NStep v-for="step in steps" :key="step.key" :title="step.title" />
|
||||
</NSteps>
|
||||
|
||||
<div class="flex-1 overflow-auto py-4">
|
||||
<!-- 根据当前步骤显示对应的组件 -->
|
||||
<BasicInfo
|
||||
v-show="currentStep === 1"
|
||||
ref="basicInfoRef"
|
||||
:table="formData.table"
|
||||
/>
|
||||
<ColumnInfo
|
||||
v-show="currentStep === 2"
|
||||
ref="columnInfoRef"
|
||||
:columns="formData.columns"
|
||||
/>
|
||||
<GenerationInfo
|
||||
v-show="currentStep === 3"
|
||||
ref="generateInfoRef"
|
||||
:table="formData.table"
|
||||
:columns="formData.columns"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<NButton :disabled="currentStep === 1" @click="prevStep">
|
||||
上一步
|
||||
</NButton>
|
||||
<NButton :disabled="currentStep === steps.length" @click="nextStep">
|
||||
下一步
|
||||
</NButton>
|
||||
<NButton type="primary" :loading="loading" @click="submitForm">
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
283
apps/web-naive/src/views/infra/codegen/index.vue
Normal file
283
apps/web-naive/src/views/infra/codegen/index.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCodegenTable,
|
||||
deleteCodegenTableList,
|
||||
downloadCodegen,
|
||||
getCodegenTablePage,
|
||||
syncCodegenFromDB,
|
||||
} from '#/api/infra/codegen';
|
||||
import { getDataSourceConfigList } from '#/api/infra/data-source-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ImportTable from './modules/import-table.vue';
|
||||
import PreviewCode from './modules/preview-code.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const dataSourceConfigList = ref<InfraDataSourceConfigApi.DataSourceConfig[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
/** 获取数据源名称 */
|
||||
const getDataSourceConfigName = (dataSourceConfigId: number) => {
|
||||
return dataSourceConfigList.value.find(
|
||||
(item) => item.id === dataSourceConfigId,
|
||||
)?.name;
|
||||
};
|
||||
|
||||
const [ImportModal, importModalApi] = useVbenModal({
|
||||
connectedComponent: ImportTable,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [PreviewModal, previewModalApi] = useVbenModal({
|
||||
connectedComponent: PreviewCode,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导入表格 */
|
||||
function handleImport() {
|
||||
importModalApi.open();
|
||||
}
|
||||
|
||||
/** 预览代码 */
|
||||
function handlePreview(row: InfraCodegenApi.CodegenTable) {
|
||||
previewModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 编辑表格 */
|
||||
function handleEdit(row: InfraCodegenApi.CodegenTable) {
|
||||
router.push({ name: 'InfraCodegenEdit', query: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 删除代码生成配置 */
|
||||
async function handleDelete(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading('正在删除...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteCodegenTable(row.id);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.tableName]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除代码生成配置 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading('正在删除...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteCodegenTableList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraCodegenApi.CodegenTable[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 同步数据库 */
|
||||
async function handleSync(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.updating', [row.tableName]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await syncCodegenFromDB(row.id);
|
||||
message.success($t('ui.actionMessage.updateSuccess', [row.tableName]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成代码 */
|
||||
async function handleGenerate(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading('正在生成代码...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
const res = await downloadCodegen(row.id);
|
||||
const blob = new Blob([res], { type: 'application/zip' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `codegen-${row.className}.zip`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
message.success('代码生成成功');
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(getDataSourceConfigName),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCodegenTablePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.CodegenTable>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
/** 获取数据源配置列表 */
|
||||
// TODO @芋艿:这种场景的最佳实践;
|
||||
async function initDataSourceConfig() {
|
||||
try {
|
||||
dataSourceConfigList.value = await getDataSourceConfigList();
|
||||
} catch (error) {
|
||||
console.error('获取数据源配置失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
initDataSourceConfig();
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="代码生成(单表)"
|
||||
url="https://doc.iocoder.cn/new-feature/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="代码生成(树表)"
|
||||
url="https://doc.iocoder.cn/new-feature/tree/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="代码生成(主子表)"
|
||||
url="https://doc.iocoder.cn/new-feature/master-sub/"
|
||||
/>
|
||||
<DocAlert title="单元测试" url="https://doc.iocoder.cn/unit-test/" />
|
||||
</template>
|
||||
|
||||
<ImportModal @success="handleRefresh" />
|
||||
<PreviewModal />
|
||||
<Grid table-title="代码生成列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.import'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:codegen:create'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:codegen:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '预览',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:codegen:preview'],
|
||||
onClick: handlePreview.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '生成代码',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:codegen:download'],
|
||||
onClick: handleGenerate.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
auth: ['infra:codegen:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '同步',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
auth: ['infra:codegen:update'],
|
||||
onClick: handleSync.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
auth: ['infra:codegen:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.tableName]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { useBasicInfoFormSchema } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
table: InfraCodegenApi.CodegenTable;
|
||||
}>();
|
||||
|
||||
/** 表单实例 */
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
schema: useBasicInfoFormSchema(),
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 动态更新表单值 */
|
||||
watch(
|
||||
() => props.table,
|
||||
(val: any) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(val);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
return valid;
|
||||
},
|
||||
getValues: formApi.getValues,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
153
apps/web-naive/src/views/infra/codegen/modules/column-info.vue
Normal file
153
apps/web-naive/src/views/infra/codegen/modules/column-info.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { NCheckbox, NInput, NSelect } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getSimpleDictTypeList } from '#/api/system/dict/type';
|
||||
|
||||
import { useCodegenColumnTableColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
columns?: InfraCodegenApi.CodegenColumn[];
|
||||
}>();
|
||||
|
||||
/** 表格配置 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useCodegenColumnTableColumns(),
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听外部传入的列数据 */
|
||||
watch(
|
||||
() => props.columns,
|
||||
async (columns) => {
|
||||
if (!columns) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
gridApi.grid?.loadData(columns);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 提供获取表格数据的方法供父组件调用 */
|
||||
defineExpose({
|
||||
getData: (): InfraCodegenApi.CodegenColumn[] => gridApi.grid.getData(),
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
const dictTypeOptions = ref<SelectOption[]>([]); // 字典类型选项
|
||||
|
||||
async function initDictTypeOptions() {
|
||||
const res = await getSimpleDictTypeList();
|
||||
dictTypeOptions.value = res.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
};
|
||||
});
|
||||
}
|
||||
onMounted(async () => {
|
||||
await initDictTypeOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<!-- 字段描述 -->
|
||||
<template #columnComment="{ row }">
|
||||
<NInput v-model:value="row.columnComment" />
|
||||
</template>
|
||||
|
||||
<!-- Java 类型 -->
|
||||
<template #javaType="{ row, column }">
|
||||
<NSelect
|
||||
v-model:value="row.javaType"
|
||||
:options="column.params.options"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
<!-- Java 属性 -->
|
||||
<template #javaField="{ row }">
|
||||
<NInput v-model:value="row.javaField" />
|
||||
</template>
|
||||
|
||||
<!-- 插入 -->
|
||||
<template #createOperation="{ row }">
|
||||
<NCheckbox v-model:checked="row.createOperation" />
|
||||
</template>
|
||||
<!-- 编辑 -->
|
||||
<template #updateOperation="{ row }">
|
||||
<NCheckbox v-model:checked="row.updateOperation" />
|
||||
</template>
|
||||
<!-- 列表 -->
|
||||
<template #listOperationResult="{ row }">
|
||||
<NCheckbox v-model:checked="row.listOperationResult" />
|
||||
</template>
|
||||
<!-- 查询 -->
|
||||
<template #listOperation="{ row }">
|
||||
<NCheckbox v-model:checked="row.listOperation" />
|
||||
</template>
|
||||
|
||||
<!-- 查询方式 -->
|
||||
<template #listOperationCondition="{ row, column }">
|
||||
<NSelect
|
||||
v-model:value="row.listOperationCondition"
|
||||
:options="column.params.options"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 允许空 -->
|
||||
<template #nullable="{ row }">
|
||||
<NCheckbox v-model:checked="row.nullable" />
|
||||
</template>
|
||||
|
||||
<!-- 显示类型 -->
|
||||
<template #htmlType="{ row, column }">
|
||||
<NSelect
|
||||
v-model:value="row.htmlType"
|
||||
:options="column.params.options"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 字典类型 -->
|
||||
<template #dictType="{ row }">
|
||||
<NSelect
|
||||
v-model:value="row.dictType"
|
||||
:options="dictTypeOptions"
|
||||
class="w-full"
|
||||
allow-clear
|
||||
show-search
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 示例 -->
|
||||
<template #example="{ row }">
|
||||
<NInput v-model:value="row.example" />
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,176 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { InfraCodegenTemplateTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getCodegenTableList } from '#/api/infra/codegen';
|
||||
|
||||
import {
|
||||
useGenerationInfoBaseFormSchema,
|
||||
useGenerationInfoSubTableFormSchema,
|
||||
useGenerationInfoTreeFormSchema,
|
||||
} from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
columns?: InfraCodegenApi.CodegenColumn[];
|
||||
table?: InfraCodegenApi.CodegenTable;
|
||||
}>();
|
||||
|
||||
const tables = ref<InfraCodegenApi.CodegenTable[]>([]);
|
||||
|
||||
/** 计算当前模板类型 */
|
||||
const currentTemplateType = ref<number>();
|
||||
const isTreeTable = computed(
|
||||
() => currentTemplateType.value === InfraCodegenTemplateTypeEnum.TREE,
|
||||
);
|
||||
const isSubTable = computed(
|
||||
() => currentTemplateType.value === InfraCodegenTemplateTypeEnum.SUB,
|
||||
);
|
||||
|
||||
/** 基础表单实例 */
|
||||
const [BaseForm, baseFormApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: useGenerationInfoBaseFormSchema(),
|
||||
handleValuesChange: (values) => {
|
||||
// 监听模板类型变化
|
||||
if (
|
||||
values.templateType !== undefined &&
|
||||
values.templateType !== currentTemplateType.value
|
||||
) {
|
||||
currentTemplateType.value = values.templateType;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 树表信息表单实例 */
|
||||
const [TreeForm, treeFormApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: [],
|
||||
});
|
||||
|
||||
/** 主子表信息表单实例 */
|
||||
const [SubForm, subFormApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: [],
|
||||
});
|
||||
|
||||
/** 更新树表信息表单 schema */
|
||||
function updateTreeSchema(): void {
|
||||
treeFormApi.setState({
|
||||
schema: useGenerationInfoTreeFormSchema(props.columns),
|
||||
});
|
||||
// 树表信息回显
|
||||
treeFormApi.setValues(props.table as any);
|
||||
}
|
||||
|
||||
/** 更新主子表信息表单 schema */
|
||||
function updateSubSchema(): void {
|
||||
subFormApi.setState({
|
||||
schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value),
|
||||
});
|
||||
// 主子表信息回显
|
||||
subFormApi.setValues(props.table as any);
|
||||
}
|
||||
|
||||
/** 获取合并的表单值 */
|
||||
async function getAllFormValues(): Promise<Record<string, any>> {
|
||||
// 基础表单值
|
||||
const baseValues = await baseFormApi.getValues();
|
||||
// 根据模板类型获取对应的额外表单值
|
||||
let extraValues = {};
|
||||
if (isTreeTable.value) {
|
||||
extraValues = await treeFormApi.getValues();
|
||||
} else if (isSubTable.value) {
|
||||
extraValues = await subFormApi.getValues();
|
||||
}
|
||||
// 合并表单值
|
||||
return { ...baseValues, ...extraValues };
|
||||
}
|
||||
|
||||
/** 验证所有表单 */
|
||||
async function validateAllForms() {
|
||||
// 验证基础表单
|
||||
const { valid: baseFormValid } = await baseFormApi.validate();
|
||||
// 根据模板类型验证对应的额外表单
|
||||
let extraValid = true;
|
||||
if (isTreeTable.value) {
|
||||
const { valid: treeFormValid } = await treeFormApi.validate();
|
||||
extraValid = treeFormValid;
|
||||
} else if (isSubTable.value) {
|
||||
const { valid: subFormValid } = await subFormApi.validate();
|
||||
extraValid = subFormValid;
|
||||
}
|
||||
return baseFormValid && extraValid;
|
||||
}
|
||||
|
||||
/** 设置表单值 */
|
||||
function setAllFormValues(values: Record<string, any>): void {
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录模板类型
|
||||
currentTemplateType.value = values.templateType;
|
||||
|
||||
// 设置基础表单值
|
||||
baseFormApi.setValues(values);
|
||||
// 根据模板类型设置对应的额外表单值
|
||||
if (isTreeTable.value) {
|
||||
treeFormApi.setValues(values);
|
||||
} else if (isSubTable.value) {
|
||||
subFormApi.setValues(values);
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听表格数据变化 */
|
||||
watch(
|
||||
() => props.table,
|
||||
async (val) => {
|
||||
if (!val || isEmpty(val)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = val as InfraCodegenApi.CodegenTable;
|
||||
// 初始化树表的 schema
|
||||
updateTreeSchema();
|
||||
// 设置表单值
|
||||
setAllFormValues(table);
|
||||
// 获取表数据,用于主子表选择
|
||||
const dataSourceConfigId = table.dataSourceConfigId;
|
||||
if (dataSourceConfigId === undefined) {
|
||||
return;
|
||||
}
|
||||
tables.value = await getCodegenTableList(dataSourceConfigId);
|
||||
// 初始化子表 schema
|
||||
updateSubSchema();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: validateAllForms,
|
||||
getValues: getAllFormValues,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 基础表单 -->
|
||||
<BaseForm />
|
||||
<!-- 树表信息表单 -->
|
||||
<TreeForm v-if="isTreeTable" />
|
||||
<!-- 主子表信息表单 -->
|
||||
<SubForm v-if="isSubTable" />
|
||||
</div>
|
||||
</template>
|
||||
118
apps/web-naive/src/views/infra/codegen/modules/import-table.vue
Normal file
118
apps/web-naive/src/views/infra/codegen/modules/import-table.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { createCodegenList, getSchemaTableList } from '#/api/infra/codegen';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
useImportTableColumns,
|
||||
useImportTableFormSchema,
|
||||
} from '#/views/infra/codegen/data';
|
||||
|
||||
/** 定义组件事件 */
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
|
||||
dataSourceConfigId: 0,
|
||||
tableNames: [], // 已选择的表列表
|
||||
});
|
||||
|
||||
/** 表格实例 */
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useImportTableFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useImportTableColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (formValues.dataSourceConfigId === undefined) {
|
||||
return [];
|
||||
}
|
||||
formData.dataSourceConfigId = formValues.dataSourceConfigId;
|
||||
return await getSchemaTableList({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'name',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
range: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.DatabaseTable>,
|
||||
gridEvents: {
|
||||
checkboxChange: ({
|
||||
records,
|
||||
}: {
|
||||
records: InfraCodegenApi.DatabaseTable[];
|
||||
}) => {
|
||||
formData.tableNames = records.map((item) => item.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 模态框实例 */
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '导入表',
|
||||
class: 'w-1/2',
|
||||
async onConfirm() {
|
||||
modalApi.lock();
|
||||
// 1.1 获取表单值
|
||||
if (formData?.dataSourceConfigId === undefined) {
|
||||
message.error('请选择数据源');
|
||||
return;
|
||||
}
|
||||
// 1.2 校验是否选择了表
|
||||
if (formData.tableNames.length === 0) {
|
||||
message.error('请选择需要导入的表');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 提交请求
|
||||
const hideLoading = message.loading('导入中...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await createCodegenList(formData);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal>
|
||||
<Grid />
|
||||
</Modal>
|
||||
</template>
|
||||
268
apps/web-naive/src/views/infra/codegen/modules/preview-code.vue
Normal file
268
apps/web-naive/src/views/infra/codegen/modules/preview-code.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TreeOption, TreeOverrideNodeClickBehaviorReturn } from 'naive-ui';
|
||||
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { CodeEditor } from '@vben/plugins/code-editor';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { NButton, NTabPane, NTabs, NTree } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { previewCodegen } from '#/api/infra/codegen';
|
||||
|
||||
/** 文件树类型 */
|
||||
interface FileNode {
|
||||
key: string;
|
||||
title: string;
|
||||
parentKey: string;
|
||||
isLeaf?: boolean;
|
||||
children?: FileNode[];
|
||||
}
|
||||
|
||||
/** 组件状态 */
|
||||
const loading = ref(false);
|
||||
const fileTree = ref<FileNode[]>([]);
|
||||
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
|
||||
const activeKey = ref<string>('');
|
||||
|
||||
/** 代码地图 */
|
||||
const codeMap = ref<Map<string, string>>(new Map<string, string>());
|
||||
function setCodeMap(key: string, code: string) {
|
||||
// 处理可能的缩进问题,特别是对Java文件
|
||||
const trimmedCode = code.trimStart();
|
||||
// 如果已有缓存则不重新构建
|
||||
if (codeMap.value.has(key)) {
|
||||
return;
|
||||
}
|
||||
codeMap.value.set(key, trimmedCode);
|
||||
}
|
||||
|
||||
/** 删除代码地图 */
|
||||
function removeCodeMapKey(targetKey: any) {
|
||||
// 只有一个代码视图时不允许删除
|
||||
if (codeMap.value.size === 1) {
|
||||
return;
|
||||
}
|
||||
if (codeMap.value.has(targetKey)) {
|
||||
codeMap.value.delete(targetKey);
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制代码 */
|
||||
async function copyCode() {
|
||||
const { copy } = useClipboard();
|
||||
const file = previewFiles.value.find(
|
||||
(item) => item.filePath === activeKey.value,
|
||||
);
|
||||
if (file) {
|
||||
await copy(file.code);
|
||||
message.success('复制成功');
|
||||
}
|
||||
}
|
||||
|
||||
/** 文件节点点击事件 */
|
||||
function handleNodeClick({
|
||||
option,
|
||||
}: {
|
||||
option: TreeOption;
|
||||
}): TreeOverrideNodeClickBehaviorReturn {
|
||||
if (!option.isLeaf) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
activeKey.value = option.key?.toString() || '';
|
||||
const file = previewFiles.value.find((item) => {
|
||||
const list = activeKey.value.split('.');
|
||||
// 特殊处理 - 包合并
|
||||
if (list.length > 2) {
|
||||
const lang = list.pop();
|
||||
return item.filePath === `${list.join('/')}.${lang}`;
|
||||
}
|
||||
return item.filePath === activeKey.value;
|
||||
});
|
||||
if (!file) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
setCodeMap(activeKey.value, file.code);
|
||||
return 'default';
|
||||
}
|
||||
|
||||
/** 处理文件树 */
|
||||
function handleFiles(data: InfraCodegenApi.CodegenPreview[]): FileNode[] {
|
||||
const exists: Record<string, boolean> = {};
|
||||
const files: FileNode[] = [];
|
||||
|
||||
// 处理文件路径
|
||||
for (const item of data) {
|
||||
const paths = item.filePath.split('/');
|
||||
let cursor = 0;
|
||||
let fullPath = '';
|
||||
|
||||
while (cursor < paths.length) {
|
||||
const path = paths[cursor] || '';
|
||||
const oldFullPath = fullPath;
|
||||
|
||||
// 处理 Java 包路径特殊情况
|
||||
if (path === 'java' && cursor + 1 < paths.length) {
|
||||
fullPath = fullPath ? `${fullPath}/${path}` : path;
|
||||
cursor++;
|
||||
|
||||
// 合并包路径
|
||||
let packagePath = '';
|
||||
while (cursor < paths.length) {
|
||||
const nextPath = paths[cursor] || '';
|
||||
if (
|
||||
[
|
||||
'controller',
|
||||
'convert',
|
||||
'dal',
|
||||
'dataobject',
|
||||
'enums',
|
||||
'mysql',
|
||||
'service',
|
||||
'vo',
|
||||
].includes(nextPath)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
packagePath = packagePath ? `${packagePath}.${nextPath}` : nextPath;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
if (packagePath) {
|
||||
const newFullPath = `${fullPath}/${packagePath}`;
|
||||
if (!exists[newFullPath]) {
|
||||
exists[newFullPath] = true;
|
||||
files.push({
|
||||
key: newFullPath,
|
||||
title: packagePath,
|
||||
parentKey: oldFullPath || '/',
|
||||
isLeaf: cursor === paths.length,
|
||||
});
|
||||
}
|
||||
fullPath = newFullPath;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理普通路径
|
||||
fullPath = fullPath ? `${fullPath}/${path}` : path;
|
||||
if (!exists[fullPath]) {
|
||||
exists[fullPath] = true;
|
||||
files.push({
|
||||
key: fullPath,
|
||||
title: path,
|
||||
parentKey: oldFullPath || '/',
|
||||
isLeaf: cursor === paths.length - 1,
|
||||
});
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建树形结构 */
|
||||
function buildTree(parentKey: string): FileNode[] {
|
||||
return files
|
||||
.filter((file) => file.parentKey === parentKey)
|
||||
.map((file) => ({
|
||||
...file,
|
||||
children: buildTree(file.key),
|
||||
}));
|
||||
}
|
||||
|
||||
return buildTree('/');
|
||||
}
|
||||
|
||||
/** 模态框实例 */
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
footer: false,
|
||||
fullscreen: true,
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
// 关闭时清除代码视图缓存
|
||||
codeMap.value.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = modalApi.getData<InfraCodegenApi.CodegenTable>();
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载预览数据
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await previewCodegen(row.id);
|
||||
previewFiles.value = data;
|
||||
|
||||
// 构建代码树,并默认选中第一个文件
|
||||
fileTree.value = handleFiles(data);
|
||||
if (data.length > 0) {
|
||||
activeKey.value = data[0]?.filePath || '';
|
||||
const code = data[0]?.code || '';
|
||||
setCodeMap(activeKey.value, code);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="代码预览">
|
||||
<div class="flex h-full" v-loading="loading">
|
||||
<!-- 文件树 -->
|
||||
<div
|
||||
class="h-full w-1/3 overflow-auto border-r border-gray-200 pr-4 dark:border-gray-700"
|
||||
>
|
||||
<NTree
|
||||
v-if="fileTree.length > 0"
|
||||
:data="fileTree"
|
||||
default-expand-all
|
||||
key-field="key"
|
||||
label-field="title"
|
||||
children-field="children"
|
||||
:checked-keys="[activeKey]"
|
||||
:override-default-node-click-behavior="handleNodeClick"
|
||||
/>
|
||||
</div>
|
||||
<!-- 代码预览 -->
|
||||
<div class="h-full w-2/3 overflow-auto pl-4">
|
||||
<NTabs v-model:value="activeKey" type="card" @close="removeCodeMapKey">
|
||||
<NTabPane
|
||||
v-for="key in codeMap.keys()"
|
||||
:name="key"
|
||||
:key="key"
|
||||
:tab="key.split('/').pop()"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-md bg-gray-50 !p-0 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<CodeEditor
|
||||
class="max-h-200"
|
||||
:value="codeMap.get(activeKey)"
|
||||
mode="application/json"
|
||||
:readonly="true"
|
||||
:bordered="true"
|
||||
:auto-format="false"
|
||||
/>
|
||||
</div>
|
||||
</NTabPane>
|
||||
<template #suffix>
|
||||
<NButton type="primary" ghost @click="copyCode">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
复制代码
|
||||
</NButton>
|
||||
</template>
|
||||
</NTabs>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
188
apps/web-naive/src/views/infra/config/data.ts
Normal file
188
apps/web-naive/src/views/infra/config/data.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'category',
|
||||
label: '参数分类',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数分类',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '参数名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'key',
|
||||
label: '参数键名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
label: '参数键值',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键值',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'visible',
|
||||
label: '是否可见',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: true,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '参数名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'key',
|
||||
label: '参数键名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键名',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '系统内置',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE, 'number'),
|
||||
placeholder: '请选择系统内置',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '参数主键',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'category',
|
||||
title: '参数分类',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '参数名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'key',
|
||||
title: '参数键名',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '参数键值',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'visible',
|
||||
title: '是否可见',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '系统内置',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_CONFIG_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
183
apps/web-naive/src/views/infra/config/index.vue
Normal file
183
apps/web-naive/src/views/infra/config/index.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraConfigApi } from '#/api/infra/config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteConfig,
|
||||
deleteConfigList,
|
||||
exportConfig,
|
||||
getConfigPage,
|
||||
} from '#/api/infra/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportConfig(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '参数配置.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建参数 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑参数 */
|
||||
function handleEdit(row: InfraConfigApi.Config) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除参数 */
|
||||
async function handleDelete(row: InfraConfigApi.Config) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteConfig(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除参数 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteConfigList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraConfigApi.Config[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraConfigApi.Config>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="参数列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['参数']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:config:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:config:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:config:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:config:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
81
apps/web-naive/src/views/infra/config/modules/form.vue
Normal file
81
apps/web-naive/src/views/infra/config/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraConfigApi } from '#/api/infra/config';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createConfig, getConfig, updateConfig } from '#/api/infra/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<InfraConfigApi.Config>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['参数'])
|
||||
: $t('ui.actionTitle.create', ['参数']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as InfraConfigApi.Config;
|
||||
try {
|
||||
await (formData.value?.id ? updateConfig(data) : createConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraConfigApi.Config>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getConfig(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
92
apps/web-naive/src/views/infra/dataSourceConfig/data.ts
Normal file
92
apps/web-naive/src/views/infra/dataSourceConfig/data.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '数据源名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入数据源名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'url',
|
||||
label: '数据源连接',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入数据源连接',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
type: 'password',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '主键编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '数据源名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
title: '数据源连接',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
title: '用户名',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
159
apps/web-naive/src/views/infra/dataSourceConfig/index.vue
Normal file
159
apps/web-naive/src/views/infra/dataSourceConfig/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDataSourceConfig,
|
||||
deleteDataSourceConfigList,
|
||||
getDataSourceConfigList,
|
||||
} from '#/api/infra/data-source-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建数据源 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑数据源 */
|
||||
function handleEdit(row: InfraDataSourceConfigApi.DataSourceConfig) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除数据源 */
|
||||
async function handleDelete(row: InfraDataSourceConfigApi.DataSourceConfig) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDataSourceConfig(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除数据源 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDataSourceConfigList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: getDataSourceConfigList,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<InfraDataSourceConfigApi.DataSourceConfig>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="数据源列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['数据源']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:data-source-config:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:data-source-config:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:data-source-config:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:data-source-config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDataSourceConfig,
|
||||
getDataSourceConfig,
|
||||
updateDataSourceConfig,
|
||||
} from '#/api/infra/data-source-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<InfraDataSourceConfigApi.DataSourceConfig>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['数据源'])
|
||||
: $t('ui.actionTitle.create', ['数据源']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as InfraDataSourceConfigApi.DataSourceConfig;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDataSourceConfig(data)
|
||||
: createDataSourceConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraDataSourceConfigApi.DataSourceConfig>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDataSourceConfig(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
152
apps/web-naive/src/views/infra/demo/demo01/data.ts
Normal file
152
apps/web-naive/src/views/infra/demo/demo01/data.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
rules: 'required',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'birthday',
|
||||
label: '出生年',
|
||||
rules: 'required',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
rules: 'required',
|
||||
component: 'RichTextarea',
|
||||
},
|
||||
{
|
||||
fieldName: 'avatar',
|
||||
label: '头像',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo01ContactApi.Demo01Contact>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'birthday',
|
||||
title: '出生年',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '简介',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '头像',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
185
apps/web-naive/src/views/infra/demo/demo01/index.vue
Normal file
185
apps/web-naive/src/views/infra/demo/demo01/index.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo01Contact,
|
||||
deleteDemo01ContactList,
|
||||
exportDemo01Contact,
|
||||
getDemo01ContactPage,
|
||||
} from '#/api/infra/demo/demo01';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo01Contact(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '示例联系人.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建示例联系人 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑示例联系人 */
|
||||
function handleEdit(row: Demo01ContactApi.Demo01Contact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除示例联系人 */
|
||||
async function handleDelete(row: Demo01ContactApi.Demo01Contact) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.id]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDemo01Contact(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除示例联系人 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo01ContactList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo01ContactApi.Demo01Contact[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDemo01ContactPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo01ContactApi.Demo01Contact>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="示例联系人列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['示例联系人']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo01-contact:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo01-contact:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:demo01-contact:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo01-contact:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo01-contact:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
87
apps/web-naive/src/views/infra/demo/demo01/modules/form.vue
Normal file
87
apps/web-naive/src/views/infra/demo/demo01/modules/form.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo01Contact,
|
||||
getDemo01Contact,
|
||||
updateDemo01Contact,
|
||||
} from '#/api/infra/demo/demo01';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo01ContactApi.Demo01Contact>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['示例联系人'])
|
||||
: $t('ui.actionTitle.create', ['示例联系人']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo01ContactApi.Demo01Contact;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo01Contact(data)
|
||||
: createDemo01Contact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<Demo01ContactApi.Demo01Contact>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDemo01Contact(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
120
apps/web-naive/src/views/infra/demo/demo02/data.ts
Normal file
120
apps/web-naive/src/views/infra/demo/demo02/data.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { getDemo02CategoryList } from '#/api/infra/demo/demo02';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '上级示例分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: async () => {
|
||||
const data = await getDemo02CategoryList({});
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级示例分类',
|
||||
});
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级示例分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '父级编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入父级编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo02CategoryApi.Demo02Category>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'parentId',
|
||||
title: '父级编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
175
apps/web-naive/src/views/infra/demo/demo02/index.vue
Normal file
175
apps/web-naive/src/views/infra/demo/demo02/index.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo02Category,
|
||||
exportDemo02Category,
|
||||
getDemo02CategoryList,
|
||||
} from '#/api/infra/demo/demo02';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo02Category(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '示例分类.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建示例分类 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑示例分类 */
|
||||
function handleEdit(row: Demo02CategoryApi.Demo02Category) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 添加下级示例分类 */
|
||||
function handleAppend(row: Demo02CategoryApi.Demo02Category) {
|
||||
formModalApi.setData({ parentId: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除示例分类 */
|
||||
async function handleDelete(row: Demo02CategoryApi.Demo02Category) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDemo02Category(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
function handleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
expandAll: true,
|
||||
reserve: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues) => {
|
||||
return await getDemo02CategoryList(formValues);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo02CategoryApi.Demo02Category>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="示例分类列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['示例分类']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo02-category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: isExpanded ? '收缩' : '展开',
|
||||
type: 'primary',
|
||||
onClick: handleExpand,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo02-category:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增下级',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo02-category:create'],
|
||||
onClick: handleAppend.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo02-category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo02-category:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
90
apps/web-naive/src/views/infra/demo/demo02/modules/form.vue
Normal file
90
apps/web-naive/src/views/infra/demo/demo02/modules/form.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo02Category,
|
||||
getDemo02Category,
|
||||
updateDemo02Category,
|
||||
} from '#/api/infra/demo/demo02';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo02CategoryApi.Demo02Category>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['示例分类'])
|
||||
: $t('ui.actionTitle.create', ['示例分类']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as Demo02CategoryApi.Demo02Category;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo02Category(data)
|
||||
: createDemo02Category(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<Demo02CategoryApi.Demo02Category>();
|
||||
if (!data || !data.id) {
|
||||
// 设置上级
|
||||
await formApi.setValues(data);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDemo02Category(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
381
apps/web-naive/src/views/infra/demo/demo03/erp/data.ts
Normal file
381
apps/web-naive/src/views/infra/demo/demo03/erp/data.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
rules: 'required',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'birthday',
|
||||
label: '出生日期',
|
||||
rules: 'required',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
rules: 'required',
|
||||
component: 'RichTextarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入简介',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'birthday',
|
||||
title: '出生日期',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '简介',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 子表(学生课程) ====================
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useDemo03CourseFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'score',
|
||||
label: '分数',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分数',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useDemo03CourseGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'studentId',
|
||||
label: '学生编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入学生编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'score',
|
||||
label: '分数',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入分数',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useDemo03CourseGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'studentId',
|
||||
title: '学生编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'score',
|
||||
title: '分数',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班级) ====================
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useDemo03GradeFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'teacher',
|
||||
label: '班主任',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入班主任',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useDemo03GradeGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'studentId',
|
||||
label: '学生编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入学生编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'teacher',
|
||||
label: '班主任',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入班主任',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useDemo03GradeGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Grade>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'studentId',
|
||||
title: '学生编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'teacher',
|
||||
title: '班主任',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
209
apps/web-naive/src/views/infra/demo/demo03/erp/index.vue
Normal file
209
apps/web-naive/src/views/infra/demo/demo03/erp/index.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { NTabPane, NTabs } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo03Student,
|
||||
deleteDemo03StudentList,
|
||||
exportDemo03Student,
|
||||
getDemo03StudentPage,
|
||||
} from '#/api/infra/demo/demo03/erp';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Demo03CourseList from './modules/demo03-course-list.vue';
|
||||
import Demo03GradeList from './modules/demo03-grade-list.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('demo03Course');
|
||||
const selectDemo03Student = ref<Demo03StudentApi.Demo03Student>();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo03Student(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: Demo03StudentApi.Demo03Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Student) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.id]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDemo03Student(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo03StudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo03StudentApi.Demo03Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: '600px',
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDemo03StudentPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
isCurrent: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
|
||||
gridEvents: {
|
||||
cellClick: ({ row }: { row: Demo03StudentApi.Demo03Student }) => {
|
||||
selectDemo03Student.value = row;
|
||||
},
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<div>
|
||||
<Grid table-title="学生列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['学生']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo03-student:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo03-student:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo03-student:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
text: true,
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 子表的表单 -->
|
||||
<NTabs v-model:value="subTabsName" class="mt-2">
|
||||
<NTabPane name="demo03Course" tab="学生课程" force-render>
|
||||
<Demo03CourseList :student-id="selectDemo03Student?.id" />
|
||||
</NTabPane>
|
||||
<NTabPane name="demo03Grade" tab="学生班级" force-render>
|
||||
<Demo03GradeList :student-id="selectDemo03Student?.id" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo03Course,
|
||||
getDemo03Course,
|
||||
updateDemo03Course,
|
||||
} from '#/api/infra/demo/demo03/erp';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDemo03CourseFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo03StudentApi.Demo03Course>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生课程'])
|
||||
: $t('ui.actionTitle.create', ['学生课程']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useDemo03CourseFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Course;
|
||||
data.studentId = formData.value?.studentId;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo03Course(data)
|
||||
: createDemo03Course(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<Demo03StudentApi.Demo03Course>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getDemo03Course(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(formData.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,197 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo03Course,
|
||||
deleteDemo03CourseList,
|
||||
getDemo03CoursePage,
|
||||
} from '#/api/infra/demo/demo03/erp';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import {
|
||||
useDemo03CourseGridColumns,
|
||||
useDemo03CourseGridFormSchema,
|
||||
} from '../data';
|
||||
import Demo03CourseForm from './demo03-course-form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Demo03CourseForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生课程 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId) {
|
||||
message.warning('请先选择一个学生!');
|
||||
return;
|
||||
}
|
||||
formModalApi.setData({ studentId: props.studentId }).open();
|
||||
}
|
||||
|
||||
/** 编辑学生课程 */
|
||||
function handleEdit(row: Demo03StudentApi.Demo03Course) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生课程 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Course) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.id]),
|
||||
{ duration: 0 },
|
||||
);
|
||||
try {
|
||||
await deleteDemo03Course(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生课程 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo03CourseList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo03StudentApi.Demo03Course[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useDemo03CourseGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useDemo03CourseGridColumns(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (!props.studentId) {
|
||||
return [];
|
||||
}
|
||||
return await getDemo03CoursePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
studentId: props.studentId,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
height: '600px',
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Course>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await handleRefresh();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="学生课程列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['学生课程']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo03-student:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo03-student:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createDemo03Grade,
|
||||
getDemo03Grade,
|
||||
updateDemo03Grade,
|
||||
} from '#/api/infra/demo/demo03/erp';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDemo03GradeFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo03StudentApi.Demo03Grade>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生班级'])
|
||||
: $t('ui.actionTitle.create', ['学生班级']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useDemo03GradeFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Grade;
|
||||
data.studentId = formData.value?.studentId;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo03Grade(data)
|
||||
: createDemo03Grade(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<Demo03StudentApi.Demo03Grade>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getDemo03Grade(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(formData.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,200 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo03Grade,
|
||||
deleteDemo03GradeList,
|
||||
getDemo03GradePage,
|
||||
} from '#/api/infra/demo/demo03/erp';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import {
|
||||
useDemo03GradeGridColumns,
|
||||
useDemo03GradeGridFormSchema,
|
||||
} from '../data';
|
||||
import Demo03GradeForm from './demo03-grade-form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Demo03GradeForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生班级 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId) {
|
||||
message.warning('请先选择一个学生!');
|
||||
return;
|
||||
}
|
||||
formModalApi.setData({ studentId: props.studentId }).open();
|
||||
}
|
||||
|
||||
/** 编辑学生班级 */
|
||||
function handleEdit(row: Demo03StudentApi.Demo03Grade) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生班级 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Grade) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo03Grade(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生班级 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deletingBatch'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo03GradeList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo03StudentApi.Demo03Grade[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useDemo03GradeGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useDemo03GradeGridColumns(),
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
if (!props.studentId) {
|
||||
return [];
|
||||
}
|
||||
return await getDemo03GradePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
studentId: props.studentId,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
height: '600px',
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Grade>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
async function handleRefresh() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await handleRefresh();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="学生班级列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['学生班级']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo03-student:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo03-student:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
danger: true,
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createDemo03Student,
|
||||
getDemo03Student,
|
||||
updateDemo03Student,
|
||||
} from '#/api/infra/demo/demo03/erp';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo03StudentApi.Demo03Student>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo03Student(data)
|
||||
: createDemo03Student(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<Demo03StudentApi.Demo03Student>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDemo03Student(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
276
apps/web-naive/src/views/infra/demo/demo03/inner/data.ts
Normal file
276
apps/web-naive/src/views/infra/demo/demo03/inner/data.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
rules: 'required',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'birthday',
|
||||
label: '出生日期',
|
||||
rules: 'required',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
rules: 'required',
|
||||
component: 'RichTextarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入简介',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{ type: 'expand', width: 80, slots: { content: 'expand_content' } },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'birthday',
|
||||
title: '出生日期',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '简介',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 子表(学生课程) ====================
|
||||
|
||||
/** 新增/修改列表的字段 */
|
||||
export function useDemo03CourseGridEditColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'score',
|
||||
title: '分数',
|
||||
minWidth: 120,
|
||||
slots: { default: 'score' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useDemo03CourseGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'studentId',
|
||||
title: '学生编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'score',
|
||||
title: '分数',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班级) ====================
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useDemo03GradeFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'teacher',
|
||||
label: '班主任',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入班主任',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useDemo03GradeGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Grade>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'studentId',
|
||||
title: '学生编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'teacher',
|
||||
title: '班主任',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
205
apps/web-naive/src/views/infra/demo/demo03/inner/index.vue
Normal file
205
apps/web-naive/src/views/infra/demo/demo03/inner/index.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { NTabPane, NTabs } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo03Student,
|
||||
deleteDemo03StudentList,
|
||||
exportDemo03Student,
|
||||
getDemo03StudentPage,
|
||||
} from '#/api/infra/demo/demo03/inner';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Demo03CourseList from './modules/demo03-course-list.vue';
|
||||
import Demo03GradeList from './modules/demo03-grade-list.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('demo03Course');
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.reload();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo03Student(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: Demo03StudentApi.Demo03Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Student) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.id]),
|
||||
{
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await deleteDemo03Student(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo03StudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo03StudentApi.Demo03Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDemo03StudentPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="学生列表">
|
||||
<template #expand_content="{ row }">
|
||||
<!-- 子表的表单 -->
|
||||
<NTabs v-model:value="subTabsName" class="mx-8">
|
||||
<NTabPane name="demo03Course" tab="学生课程" force-render>
|
||||
<Demo03CourseList :student-id="row?.id" />
|
||||
</NTabPane>
|
||||
<NTabPane name="demo03Grade" tab="学生班级" force-render>
|
||||
<Demo03GradeList :student-id="row?.id" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['学生']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo03-student:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo03-student:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
disabled: isEmpty(checkedIds),
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo03-student:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { NButton, NInput } from 'naive-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/inner';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDemo03CourseGridEditColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDemo03CourseGridEditColumns(),
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 添加学生课程 */
|
||||
async function handleAdd() {
|
||||
await gridApi.grid.insertAt({} as Demo03StudentApi.Demo03Course, -1);
|
||||
}
|
||||
|
||||
/** 删除学生课程 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Course) {
|
||||
await gridApi.grid.remove(row);
|
||||
}
|
||||
|
||||
/** 提供获取表格数据的方法供父组件调用 */
|
||||
defineExpose({
|
||||
getData: (): Demo03StudentApi.Demo03Course[] => {
|
||||
const data = gridApi.grid.getData() as Demo03StudentApi.Demo03Course[];
|
||||
const removeRecords =
|
||||
gridApi.grid.getRemoveRecords() as Demo03StudentApi.Demo03Course[];
|
||||
const insertRecords =
|
||||
gridApi.grid.getInsertRecords() as Demo03StudentApi.Demo03Course[];
|
||||
return [
|
||||
...data.filter(
|
||||
(row) => !removeRecords.some((removed) => removed.id === row.id),
|
||||
),
|
||||
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await gridApi.grid.loadData(
|
||||
await getDemo03CourseListByStudentId(props.studentId!),
|
||||
);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid class="mx-4">
|
||||
<template #name="{ row }">
|
||||
<NInput v-model:value="row.name" />
|
||||
</template>
|
||||
<template #score="{ row }">
|
||||
<NInput v-model:value="row.score" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
<div class="-mt-4 flex justify-center">
|
||||
<NButton
|
||||
type="primary"
|
||||
ghost
|
||||
@click="handleAdd"
|
||||
v-access:code="['infra:demo03-student:create']"
|
||||
>
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
{{ $t('ui.actionTitle.create', ['学生课程']) }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { useDemo03CourseGridColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDemo03CourseGridColumns(),
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
height: '600px',
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Course>,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
async function handleRefresh() {
|
||||
await gridApi.grid.loadData(
|
||||
await getDemo03CourseListByStudentId(props.studentId!),
|
||||
);
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await handleRefresh();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="学生课程列表" />
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { useDemo03GradeFormSchema } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useDemo03GradeFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
return valid;
|
||||
},
|
||||
getValues: formApi.getValues,
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await formApi.setValues(await getDemo03GradeByStudentId(props.studentId!));
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form class="mx-4" />
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { useDemo03GradeGridColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDemo03GradeGridColumns(),
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
height: '600px',
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Grade>,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
async function handleRefresh() {
|
||||
await gridApi.grid.loadData([
|
||||
await getDemo03GradeByStudentId(props.studentId!),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await handleRefresh();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid table-title="学生班级列表" />
|
||||
</template>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NTabPane, NTabs } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo03Student,
|
||||
getDemo03Student,
|
||||
updateDemo03Student,
|
||||
} from '#/api/infra/demo/demo03/inner';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
import Demo03CourseForm from './demo03-course-form.vue';
|
||||
import Demo03GradeForm from './demo03-grade-form.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo03StudentApi.Demo03Student>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
/** 子表的表单 */
|
||||
const subTabsName = ref('demo03Course');
|
||||
const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
|
||||
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// 校验子表单
|
||||
const demo03GradeValid = await demo03GradeFormRef.value?.validate();
|
||||
if (!demo03GradeValid) {
|
||||
subTabsName.value = 'demo03Grade';
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
|
||||
// 拼接子表的数据
|
||||
data.demo03courses = demo03CourseFormRef.value?.getData();
|
||||
data.demo03grade = await demo03GradeFormRef.value?.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo03Student(data)
|
||||
: createDemo03Student(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getDemo03Student(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(formData.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
<!-- 子表的表单 -->
|
||||
<NTabs v-model:value="subTabsName">
|
||||
<NTabPane name="demo03Course" tab="学生课程" force-render>
|
||||
<Demo03CourseForm
|
||||
ref="demo03CourseFormRef"
|
||||
:student-id="formData?.id"
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane name="demo03Grade" tab="学生班级" force-render>
|
||||
<Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData?.id" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</Modal>
|
||||
</template>
|
||||
211
apps/web-naive/src/views/infra/demo/demo03/normal/data.ts
Normal file
211
apps/web-naive/src/views/infra/demo/demo03/normal/data.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
rules: 'required',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'birthday',
|
||||
label: '出生日期',
|
||||
rules: 'required',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
rules: 'required',
|
||||
component: 'RichTextarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sex',
|
||||
label: '性别',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '简介',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入简介',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'birthday',
|
||||
title: '出生日期',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '简介',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 120,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 子表(学生课程) ====================
|
||||
|
||||
/** 新增/修改列表的字段 */
|
||||
export function useDemo03CourseGridEditColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '名字',
|
||||
minWidth: 120,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'score',
|
||||
title: '分数',
|
||||
minWidth: 120,
|
||||
slots: { default: 'score' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班级) ====================
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useDemo03GradeFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名字',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'teacher',
|
||||
label: '班主任',
|
||||
rules: 'required',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入班主任',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
187
apps/web-naive/src/views/infra/demo/demo03/normal/index.vue
Normal file
187
apps/web-naive/src/views/infra/demo/demo03/normal/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDemo03Student,
|
||||
deleteDemo03StudentList,
|
||||
exportDemo03Student,
|
||||
getDemo03StudentPage,
|
||||
} from '#/api/infra/demo/demo03/normal';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDemo03Student(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: Demo03StudentApi.Demo03Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Student) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.id]),
|
||||
{
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await deleteDemo03Student(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteDemo03StudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: Demo03StudentApi.Demo03Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDemo03StudentPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="学生列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['学生']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:demo03-student:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:demo03-student:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:demo03-student:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { NButton, NInput } from 'naive-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDemo03CourseGridEditColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDemo03CourseGridEditColumns(),
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 添加学生课程 */
|
||||
async function handleAdd() {
|
||||
await gridApi.grid.insertAt({} as Demo03StudentApi.Demo03Course, -1);
|
||||
}
|
||||
|
||||
/** 删除学生课程 */
|
||||
async function handleDelete(row: Demo03StudentApi.Demo03Course) {
|
||||
await gridApi.grid.remove(row);
|
||||
}
|
||||
|
||||
/** 提供获取表格数据的方法供父组件调用 */
|
||||
defineExpose({
|
||||
getData: (): Demo03StudentApi.Demo03Course[] => {
|
||||
const data = gridApi.grid.getData() as Demo03StudentApi.Demo03Course[];
|
||||
const removeRecords =
|
||||
gridApi.grid.getRemoveRecords() as Demo03StudentApi.Demo03Course[];
|
||||
const insertRecords =
|
||||
gridApi.grid.getInsertRecords() as Demo03StudentApi.Demo03Course[];
|
||||
return [
|
||||
...data.filter(
|
||||
(row) => !removeRecords.some((removed) => removed.id === row.id),
|
||||
),
|
||||
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await gridApi.grid.loadData(
|
||||
await getDemo03CourseListByStudentId(props.studentId!),
|
||||
);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid class="mx-4">
|
||||
<template #name="{ row }">
|
||||
<NInput v-model:value="row.name" />
|
||||
</template>
|
||||
<template #score="{ row }">
|
||||
<NInput v-model:value="row.score" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:demo03-student:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.id]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
<div class="-mt-4 flex justify-center">
|
||||
<NButton
|
||||
type="primary"
|
||||
ghost
|
||||
@click="handleAdd"
|
||||
v-access:code="['infra:demo03-student:create']"
|
||||
>
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
{{ $t('ui.actionTitle.create', ['学生课程']) }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||
|
||||
import { useDemo03GradeFormSchema } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number; // 学生编号(主表的关联字段)
|
||||
}>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useDemo03GradeFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
return valid;
|
||||
},
|
||||
getValues: formApi.getValues,
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await formApi.setValues(await getDemo03GradeByStudentId(props.studentId!));
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form class="mx-4" />
|
||||
</template>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NTabPane, NTabs } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDemo03Student,
|
||||
getDemo03Student,
|
||||
updateDemo03Student,
|
||||
} from '#/api/infra/demo/demo03/normal';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
import Demo03CourseForm from './demo03-course-form.vue';
|
||||
import Demo03GradeForm from './demo03-grade-form.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Demo03StudentApi.Demo03Student>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
/** 子表的表单 */
|
||||
const subTabsName = ref('demo03Course');
|
||||
const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
|
||||
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// 校验子表单
|
||||
const demo03GradeValid = await demo03GradeFormRef.value?.validate();
|
||||
if (!demo03GradeValid) {
|
||||
subTabsName.value = 'demo03Grade';
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
|
||||
// 拼接子表的数据
|
||||
data.demo03courses = demo03CourseFormRef.value?.getData();
|
||||
data.demo03grade = await demo03GradeFormRef.value?.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDemo03Student(data)
|
||||
: createDemo03Student(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getDemo03Student(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(formData.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
<!-- 子表的表单 -->
|
||||
<NTabs v-model:value="subTabsName">
|
||||
<NTabPane name="demo03Course" tab="学生课程" force-render>
|
||||
<Demo03CourseForm
|
||||
ref="demo03CourseFormRef"
|
||||
:student-id="formData?.id"
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane name="demo03Grade" tab="学生班级" force-render>
|
||||
<Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData?.id" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</Modal>
|
||||
</template>
|
||||
36
apps/web-naive/src/views/infra/druid/index.vue
Normal file
36
apps/web-naive/src/views/infra/druid/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, IFrame, Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
|
||||
const loading = ref(true); // 是否加载中
|
||||
const src = ref(`${import.meta.env.VITE_BASE_URL}/druid/index.html`);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await getConfigKey('url.druid');
|
||||
if (data && data.length > 0) {
|
||||
src.value = data;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="数据库 MyBatis" url="https://doc.iocoder.cn/mybatis/" />
|
||||
<DocAlert
|
||||
title="多数据源(读写分离)"
|
||||
url="https://doc.iocoder.cn/dynamic-datasource/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<IFrame v-if="!loading" v-loading="loading" :src="src" />
|
||||
</Page>
|
||||
</template>
|
||||
115
apps/web-naive/src/views/infra/file/data.ts
Normal file
115
apps/web-naive/src/views/infra/file/data.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 表单的字段 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '文件上传',
|
||||
component: 'Upload',
|
||||
componentProps: {
|
||||
placeholder: '请选择要上传的文件',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'path',
|
||||
label: '文件路径',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入文件路径',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '文件类型',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入文件类型',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'name',
|
||||
title: '文件名',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'path',
|
||||
title: '文件路径',
|
||||
minWidth: 200,
|
||||
showOverflow: true,
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
title: 'URL',
|
||||
minWidth: 200,
|
||||
showOverflow: true,
|
||||
},
|
||||
{
|
||||
field: 'size',
|
||||
title: '文件大小',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }) => {
|
||||
// TODO @xingyu:【优先级:中】要不要搞到一个方法里?
|
||||
if (!cellValue) return '0 B';
|
||||
const unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const index = Math.floor(Math.log(cellValue) / Math.log(1024));
|
||||
const size = cellValue / 1024 ** index;
|
||||
const formattedSize = size.toFixed(2);
|
||||
return `${formattedSize} ${unitArr[index]}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '文件类型',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'file-content',
|
||||
title: '文件内容',
|
||||
minWidth: 120,
|
||||
slots: {
|
||||
default: 'file-content',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '上传时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
187
apps/web-naive/src/views/infra/file/index.vue
Normal file
187
apps/web-naive/src/views/infra/file/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraFileApi } from '#/api/infra/file';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty, openWindow } from '@vben/utils';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { NButton, NImage } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteFile, deleteFileList, getFilePage } from '#/api/infra/file';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
function handleUpload() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 复制链接到剪贴板 */
|
||||
const { copy } = useClipboard({ legacy: true });
|
||||
async function handleCopyUrl(row: InfraFileApi.File) {
|
||||
if (!row.url) {
|
||||
message.error('文件 URL 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copy(row.url);
|
||||
message.success('复制成功');
|
||||
} catch {
|
||||
message.error('复制失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
async function handleDelete(row: InfraFileApi.File) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name || row.path]),
|
||||
{
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await deleteFile(row.id!);
|
||||
message.success(
|
||||
$t('ui.actionMessage.deleteSuccess', [row.name || row.path]),
|
||||
);
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除文件 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteFileList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraFileApi.File[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getFilePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraFileApi.File>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="文件列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '上传文件',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.UPLOAD,
|
||||
onClick: handleUpload,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:file:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #file-content="{ row }">
|
||||
<NImage v-if="row.type && row.type.includes('image')" :src="row.url" />
|
||||
<NButton v-else type="primary" text @click="() => openWindow(row.url!)">
|
||||
{{ row.type && row.type.includes('pdf') ? '预览' : '下载' }}
|
||||
</NButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '复制链接',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.COPY,
|
||||
onClick: handleCopyUrl.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:file:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
84
apps/web-naive/src/views/infra/file/modules/form.vue
Normal file
84
apps/web-naive/src/views/infra/file/modules/form.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { NUpload } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useUpload } from '#/components/upload/use-upload';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
hideLabel: true,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema().map((item) => ({ ...item, label: '' })), // 去除label
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await useUpload().httpRequest(data.file);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 上传前 */
|
||||
function beforeUpload(file: UploadFileInfo) {
|
||||
formApi.setFieldValue('file', file);
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="上传图片">
|
||||
<Form class="mx-4">
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<!-- 上传区域 -->
|
||||
<NUpload.Dragger
|
||||
name="file"
|
||||
:max-count="1"
|
||||
accept=".jpg,.png,.gif,.webp"
|
||||
:before-upload="beforeUpload"
|
||||
list-type="picture-card"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<span class="icon-[ant-design--inbox-outlined] text-2xl"></span>
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||
<p class="ant-upload-hint">
|
||||
支持 .jpg、.png、.gif、.webp 格式图片文件
|
||||
</p>
|
||||
</NUpload.Dragger>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
334
apps/web-naive/src/views/infra/fileConfig/data.ts
Normal file
334
apps/web-naive/src/views/infra/fileConfig/data.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '配置名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入配置名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'storage',
|
||||
label: '存储器',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE, 'number'),
|
||||
placeholder: '请选择存储器',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (formValues) => formValues.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
// DB / Local / FTP / SFTP
|
||||
{
|
||||
fieldName: 'config.basePath',
|
||||
label: '基础路径',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入基础路径',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) =>
|
||||
formValues.storage >= 10 && formValues.storage <= 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.host',
|
||||
label: '主机地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入主机地址',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) =>
|
||||
formValues.storage >= 11 && formValues.storage <= 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.port',
|
||||
label: '主机端口',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
placeholder: '请输入主机端口',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) =>
|
||||
formValues.storage >= 11 && formValues.storage <= 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.username',
|
||||
label: '用户名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) =>
|
||||
formValues.storage >= 11 && formValues.storage <= 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.password',
|
||||
label: '密码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) =>
|
||||
formValues.storage >= 11 && formValues.storage <= 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.mode',
|
||||
label: '连接模式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '主动模式', value: 'Active' },
|
||||
{ label: '被动模式', value: 'Passive' },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 11,
|
||||
},
|
||||
},
|
||||
// S3
|
||||
{
|
||||
fieldName: 'config.endpoint',
|
||||
label: '节点地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入节点地址',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.bucket',
|
||||
label: '存储 bucket',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 bucket',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.accessKey',
|
||||
label: 'accessKey',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 accessKey',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.accessSecret',
|
||||
label: 'accessSecret',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 accessSecret',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'config.enablePathStyleAccess',
|
||||
label: '是否 Path Style',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '启用', value: true },
|
||||
{ label: '禁用', value: false },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'config.enablePublicAccess',
|
||||
label: '公开访问',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '公开', value: true },
|
||||
{ label: '私有', value: false },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
// 通用
|
||||
{
|
||||
fieldName: 'config.domain',
|
||||
label: '自定义域名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入自定义域名',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => !!formValues.storage,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '配置名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入配置名',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'storage',
|
||||
label: '存储器',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE, 'number'),
|
||||
placeholder: '请选择存储器',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '配置名',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'storage',
|
||||
title: '存储器',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_FILE_STORAGE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '主配置',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
231
apps/web-naive/src/views/infra/fileConfig/index.vue
Normal file
231
apps/web-naive/src/views/infra/fileConfig/index.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraFileConfigApi } from '#/api/infra/file-config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty, openWindow } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteFileConfig,
|
||||
deleteFileConfigList,
|
||||
getFileConfigPage,
|
||||
testFileConfig,
|
||||
updateFileConfigMaster,
|
||||
} from '#/api/infra/file-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建文件配置 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑文件配置 */
|
||||
function handleEdit(row: InfraFileConfigApi.FileConfig) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 设为主配置 */
|
||||
async function handleMaster(row: InfraFileConfigApi.FileConfig) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.updating', [row.name]),
|
||||
{
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await updateFileConfigMaster(row.id!);
|
||||
message.success($t('ui.actionMessage.updateSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试文件配置 */
|
||||
async function handleTest(row: InfraFileConfigApi.FileConfig) {
|
||||
const hideLoading = message.loading('测试上传中...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
const response = await testFileConfig(row.id!);
|
||||
// 确认是否访问该文件
|
||||
confirm({
|
||||
title: '测试上传成功',
|
||||
content: '是否要访问该文件?',
|
||||
confirmText: '访问',
|
||||
cancelText: '取消',
|
||||
}).then(() => {
|
||||
openWindow(response);
|
||||
});
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除文件配置 */
|
||||
async function handleDelete(row: InfraFileConfigApi.FileConfig) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await deleteFileConfig(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除文件配置 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteFileConfigList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: InfraFileConfigApi.FileConfig[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getFileConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraFileConfigApi.FileConfig>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="文件配置列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['文件配置']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:file-config:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:file-config:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:file-config:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '测试',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: 'lucide:test-tube-diagonal',
|
||||
auth: ['infra:file-config:update'],
|
||||
onClick: handleTest.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '主配置',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:file-config:update'],
|
||||
disabled: row.master,
|
||||
popConfirm: {
|
||||
title: `是否要将${row.name}设为主配置?`,
|
||||
confirm: handleMaster.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:file-config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
87
apps/web-naive/src/views/infra/fileConfig/modules/form.vue
Normal file
87
apps/web-naive/src/views/infra/fileConfig/modules/form.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraFileConfigApi } from '#/api/infra/file-config';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createFileConfig,
|
||||
getFileConfig,
|
||||
updateFileConfig,
|
||||
} from '#/api/infra/file-config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<InfraFileConfigApi.FileConfig>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['文件配置'])
|
||||
: $t('ui.actionTitle.create', ['文件配置']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as InfraFileConfigApi.FileConfig;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateFileConfig(data)
|
||||
: createFileConfig(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraFileConfigApi.FileConfig>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getFileConfig(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
246
apps/web-naive/src/views/infra/job/data.ts
Normal file
246
apps/web-naive/src/views/infra/job/data.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
|
||||
import { CronTab } from '#/components/cron-tab';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '任务名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入任务名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'handlerName',
|
||||
label: '处理器的名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入处理器的名字',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => !!values.id,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'handlerParam',
|
||||
label: '处理器的参数',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入处理器的参数',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'cronExpression',
|
||||
label: 'CRON 表达式',
|
||||
component: markRaw(CronTab),
|
||||
componentProps: {
|
||||
placeholder: '请输入 CRON 表达式',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'retryCount',
|
||||
label: '重试次数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入重试次数。设置为 0 时,不进行重试',
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'retryInterval',
|
||||
label: '重试间隔',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔',
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'monitorTimeout',
|
||||
label: '监控超时时间',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入监控超时时间,单位:毫秒',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '任务名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入任务名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '任务状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_JOB_STATUS, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择任务状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'handlerName',
|
||||
label: '处理器的名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入处理器的名字',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'id',
|
||||
title: '任务编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '任务名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '任务状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_JOB_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'handlerName',
|
||||
title: '处理器的名字',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'handlerParam',
|
||||
title: '处理器的参数',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'cronExpression',
|
||||
title: 'CRON 表达式',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
label: '任务编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '任务名称',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '任务状态',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_JOB_STATUS,
|
||||
value: data?.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'handlerName',
|
||||
label: '处理器的名字',
|
||||
},
|
||||
{
|
||||
field: 'handlerParam',
|
||||
label: '处理器的参数',
|
||||
},
|
||||
{
|
||||
field: 'cronExpression',
|
||||
label: 'Cron 表达式',
|
||||
},
|
||||
{
|
||||
field: 'retryCount',
|
||||
label: '重试次数',
|
||||
},
|
||||
{
|
||||
label: '重试间隔',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return data?.retryInterval ? `${data.retryInterval} 毫秒` : '无间隔';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '监控超时时间',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return data?.monitorTimeout && data.monitorTimeout > 0
|
||||
? `${data.monitorTimeout} 毫秒`
|
||||
: '未开启';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nextTimes',
|
||||
label: '后续执行时间',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
if (!data?.nextTimes || data.nextTimes.length === 0) {
|
||||
return '无后续执行时间';
|
||||
}
|
||||
return h(Timeline, {}, () =>
|
||||
data.nextTimes?.map((time: Date) =>
|
||||
h(Timeline.Item, {}, () => formatDateTime(time)),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
296
apps/web-naive/src/views/infra/job/index.vue
Normal file
296
apps/web-naive/src/views/infra/job/index.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { InfraJobStatusEnum } from '@vben/constants';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteJob,
|
||||
deleteJobList,
|
||||
exportJob,
|
||||
getJobPage,
|
||||
runJob,
|
||||
updateJobStatus,
|
||||
} from '#/api/infra/job';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportJob(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '定时任务.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建任务 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑任务 */
|
||||
function handleEdit(row: InfraJobApi.Job) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 查看任务详情 */
|
||||
function handleDetail(row: InfraJobApi.Job) {
|
||||
detailModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 更新任务状态 */
|
||||
async function handleUpdateStatus(row: InfraJobApi.Job) {
|
||||
const status =
|
||||
row.status === InfraJobStatusEnum.STOP
|
||||
? InfraJobStatusEnum.NORMAL
|
||||
: InfraJobStatusEnum.STOP;
|
||||
const statusText = status === InfraJobStatusEnum.NORMAL ? '启用' : '停用';
|
||||
|
||||
await confirm(`确定${statusText} ${row.name} 吗?`);
|
||||
const hideLoading = message.loading(`正在${statusText}中...`, {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await updateJobStatus(row.id!, status);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行一次任务 */
|
||||
async function handleTrigger(row: InfraJobApi.Job) {
|
||||
await confirm(`确定执行一次 ${row.name} 吗?`);
|
||||
const hideLoading = message.loading('正在执行中...', {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await runJob(row.id!);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到任务日志 */
|
||||
function handleLog(row?: InfraJobApi.Job) {
|
||||
push({
|
||||
name: 'InfraJobLog',
|
||||
query: row?.id ? { id: row.id } : {},
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除任务 */
|
||||
async function handleDelete(row: InfraJobApi.Job) {
|
||||
const hideLoading = message.loading(
|
||||
$t('ui.actionMessage.deleting', [row.name]),
|
||||
{
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await deleteJob(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除任务 */
|
||||
async function handleDeleteBatch() {
|
||||
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
|
||||
const hideLoading = message.loading($t('ui.actionMessage.deletingBatch'), {
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteJobList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({ records }: { records: InfraJobApi.Job[] }) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getJobPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraJobApi.Job>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="定时任务" url="https://doc.iocoder.cn/job/" />
|
||||
<DocAlert title="异步任务" url="https://doc.iocoder.cn/async-task/" />
|
||||
<DocAlert title="消息队列" url="https://doc.iocoder.cn/message-queue/" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailModal />
|
||||
<Grid table-title="定时任务列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['任务']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['infra:job:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:job:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: '执行日志',
|
||||
type: 'primary',
|
||||
icon: 'lucide:history',
|
||||
auth: ['infra:job:query'],
|
||||
onClick: () => handleLog(undefined),
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.deleteBatch'),
|
||||
type: 'error',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: isEmpty(checkedIds),
|
||||
auth: ['infra:job:delete'],
|
||||
onClick: handleDeleteBatch,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:job:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '开启',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: 'lucide:circle-play',
|
||||
auth: ['infra:job:update'],
|
||||
ifShow: () => row.status === InfraJobStatusEnum.STOP,
|
||||
onClick: handleUpdateStatus.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '暂停',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: 'lucide:circle-pause',
|
||||
auth: ['infra:job:update'],
|
||||
ifShow: () => row.status === InfraJobStatusEnum.NORMAL,
|
||||
onClick: handleUpdateStatus.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '执行',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: 'lucide:clock-plus',
|
||||
auth: ['infra:job:trigger'],
|
||||
onClick: handleTrigger.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
auth: ['infra:job:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '日志',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
auth: ['infra:job:query'],
|
||||
onClick: handleLog.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'error',
|
||||
text: true,
|
||||
auth: ['infra:job:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
186
apps/web-naive/src/views/infra/job/logger/data.ts
Normal file
186
apps/web-naive/src/views/infra/job/logger/data.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'handlerName',
|
||||
label: '处理器的名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入处理器的名字',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'beginTime',
|
||||
label: '开始执行时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '选择开始执行时间',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
showTime: {
|
||||
format: 'HH:mm:ss',
|
||||
defaultValue: dayjs('00:00:00', 'HH:mm:ss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束执行时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '选择结束执行时间',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
showTime: {
|
||||
format: 'HH:mm:ss',
|
||||
defaultValue: dayjs('23:59:59', 'HH:mm:ss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '任务状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择任务状态',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'jobId',
|
||||
title: '任务编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'handlerName',
|
||||
title: '处理器的名字',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'handlerParam',
|
||||
title: '处理器的参数',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'executeIndex',
|
||||
title: '第几次执行',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'beginTime',
|
||||
title: '执行时间',
|
||||
minWidth: 280,
|
||||
formatter: ({ row }) => {
|
||||
return `${formatDateTime(row.beginTime)} ~ ${formatDateTime(row.endTime)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
title: '执行时长',
|
||||
minWidth: 120,
|
||||
formatter: ({ row }) => {
|
||||
return `${row.duration} 毫秒`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '任务状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_JOB_LOG_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
label: '日志编号',
|
||||
},
|
||||
{
|
||||
field: 'jobId',
|
||||
label: '任务编号',
|
||||
},
|
||||
{
|
||||
field: 'handlerName',
|
||||
label: '处理器的名字',
|
||||
},
|
||||
{
|
||||
field: 'handlerParam',
|
||||
label: '处理器的参数',
|
||||
},
|
||||
{
|
||||
field: 'executeIndex',
|
||||
label: '第几次执行',
|
||||
},
|
||||
{
|
||||
field: 'beginTime',
|
||||
label: '执行时间',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
return `${formatDateTime(data.beginTime)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
label: '执行时长',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
return data?.duration ? `${data.duration} 毫秒` : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '任务状态',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_JOB_LOG_STATUS,
|
||||
value: data?.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'result',
|
||||
label: '执行结果',
|
||||
},
|
||||
];
|
||||
}
|
||||
106
apps/web-naive/src/views/infra/job/logger/index.vue
Normal file
106
apps/web-naive/src/views/infra/job/logger/index.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { exportJobLog, getJobLogPage } from '#/api/infra/job-log';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const { query } = useRoute();
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportJobLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '任务日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看日志详情 */
|
||||
function handleDetail(row: InfraJobLogApi.JobLog) {
|
||||
detailModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getJobLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
jobId: query.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraJobLogApi.JobLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="定时任务" url="https://doc.iocoder.cn/job/" />
|
||||
<DocAlert title="异步任务" url="https://doc.iocoder.cn/async-task/" />
|
||||
<DocAlert title="消息队列" url="https://doc.iocoder.cn/message-queue/" />
|
||||
</template>
|
||||
|
||||
<DetailModal />
|
||||
<Grid table-title="任务日志列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['infra:job:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
text: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['infra:job:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
54
apps/web-naive/src/views/infra/job/logger/modules/detail.vue
Normal file
54
apps/web-naive/src/views/infra/job/logger/modules/detail.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { getJobLog } from '#/api/infra/job-log';
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const formData = ref<InfraJobLogApi.JobLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{ id: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getJobLog(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
59
apps/web-naive/src/views/infra/job/modules/detail.vue
Normal file
59
apps/web-naive/src/views/infra/job/modules/detail.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { getJob, getJobNextTimes } from '#/api/infra/job';
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const formData = ref<InfraJobApi.Job>(); // 任务详情
|
||||
const nextTimes = ref<Date[]>([]); // 下一次执行时间
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{ id: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getJob(data.id);
|
||||
// 获取下一次执行时间
|
||||
nextTimes.value = await getJobNextTimes(data.id);
|
||||
// 将 nextTimes 赋值给 formData,以便在 schema 中使用
|
||||
formData.value.nextTimes = nextTimes.value;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="任务详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
81
apps/web-naive/src/views/infra/job/modules/form.vue
Normal file
81
apps/web-naive/src/views/infra/job/modules/form.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createJob, getJob, updateJob } from '#/api/infra/job';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<InfraJobApi.Job>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['任务'])
|
||||
: $t('ui.actionTitle.create', ['任务']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as InfraJobApi.Job;
|
||||
try {
|
||||
await (formData.value?.id ? updateJob(data) : createJob(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<InfraJobApi.Job>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getJob(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
53
apps/web-naive/src/views/infra/redis/index.vue
Normal file
53
apps/web-naive/src/views/infra/redis/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { NCard } from 'naive-ui';
|
||||
|
||||
import { getRedisMonitorInfo } from '#/api/infra/redis';
|
||||
|
||||
import Commands from './modules/commands.vue';
|
||||
import Info from './modules/info.vue';
|
||||
import Memory from './modules/memory.vue';
|
||||
|
||||
const redisData = ref<InfraRedisApi.RedisMonitorInfo>();
|
||||
|
||||
/** 统一加载 Redis 数据 */
|
||||
async function loadRedisData() {
|
||||
try {
|
||||
redisData.value = await getRedisMonitorInfo();
|
||||
} catch (error) {
|
||||
console.error('加载 Redis 数据失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRedisData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="Redis 缓存" url="https://doc.iocoder.cn/redis-cache/" />
|
||||
<DocAlert title="本地缓存" url="https://doc.iocoder.cn/local-cache/" />
|
||||
</template>
|
||||
|
||||
<NCard title="Redis 概览">
|
||||
<Info :redis-data="redisData" />
|
||||
</NCard>
|
||||
|
||||
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<NCard title="内存使用">
|
||||
<Memory :redis-data="redisData" />
|
||||
</NCard>
|
||||
|
||||
<NCard title="命令统计">
|
||||
<Commands :redis-data="redisData" />
|
||||
</NCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
101
apps/web-naive/src/views/infra/redis/modules/commands.vue
Normal file
101
apps/web-naive/src/views/infra/redis/modules/commands.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
redisData?: InfraRedisApi.RedisMonitorInfo;
|
||||
}>();
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
/** 渲染命令统计图表 */
|
||||
function renderCommandStats() {
|
||||
if (!props.redisData?.commandStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
const commandStats = [] as any[];
|
||||
const nameList = [] as string[];
|
||||
props.redisData.commandStats.forEach((row) => {
|
||||
commandStats.push({
|
||||
name: row.command,
|
||||
value: row.calls,
|
||||
});
|
||||
nameList.push(row.command);
|
||||
});
|
||||
|
||||
// 渲染图表
|
||||
renderEcharts({
|
||||
title: {
|
||||
text: '命令统计',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 10,
|
||||
bottom: 20,
|
||||
data: nameList,
|
||||
textStyle: {
|
||||
color: '#a1a1a1',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '命令',
|
||||
type: 'pie',
|
||||
radius: [20, 120],
|
||||
center: ['40%', '60%'],
|
||||
data: commandStats,
|
||||
roseType: 'radius',
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 监听数据变化,重新渲染图表 */
|
||||
watch(
|
||||
() => props.redisData,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
renderCommandStats();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.redisData) {
|
||||
renderCommandStats();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" height="420px" />
|
||||
</template>
|
||||
60
apps/web-naive/src/views/infra/redis/modules/info.vue
Normal file
60
apps/web-naive/src/views/infra/redis/modules/info.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { NDescriptions } from 'naive-ui';
|
||||
|
||||
defineProps<{
|
||||
redisData?: InfraRedisApi.RedisMonitorInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDescriptions
|
||||
:column="6"
|
||||
bordered
|
||||
size="medium"
|
||||
:label-style="{ width: '138px' }"
|
||||
>
|
||||
<NDescriptions.Item label="Redis 版本">
|
||||
{{ redisData?.info?.redis_version }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="运行模式">
|
||||
{{ redisData?.info?.redis_mode === 'standalone' ? '单机' : '集群' }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="端口">
|
||||
{{ redisData?.info?.tcp_port }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="客户端数">
|
||||
{{ redisData?.info?.connected_clients }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="运行时间(天)">
|
||||
{{ redisData?.info?.uptime_in_days }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="使用内存">
|
||||
{{ redisData?.info?.used_memory_human }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="使用 CPU">
|
||||
{{
|
||||
redisData?.info
|
||||
? parseFloat(redisData?.info?.used_cpu_user_children).toFixed(2)
|
||||
: ''
|
||||
}}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="内存配置">
|
||||
{{ redisData?.info?.maxmemory_human }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="AOF 是否开启">
|
||||
{{ redisData?.info?.aof_enabled === '0' ? '否' : '是' }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="RDB 是否成功">
|
||||
{{ redisData?.info?.rdb_last_bgsave_status }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="Key 数量">
|
||||
{{ redisData?.dbSize }}
|
||||
</NDescriptions.Item>
|
||||
<NDescriptions.Item label="网络入口/出口">
|
||||
{{ redisData?.info?.instantaneous_input_kbps }}kps /
|
||||
{{ redisData?.info?.instantaneous_output_kbps }}kps
|
||||
</NDescriptions.Item>
|
||||
</NDescriptions>
|
||||
</template>
|
||||
135
apps/web-naive/src/views/infra/redis/modules/memory.vue
Normal file
135
apps/web-naive/src/views/infra/redis/modules/memory.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
redisData?: InfraRedisApi.RedisMonitorInfo;
|
||||
}>();
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
/** 解析内存值,移除单位,转为数字 */
|
||||
function parseMemoryValue(memStr: string | undefined): number {
|
||||
if (!memStr) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
// 从字符串中提取数字部分,例如 "1.2M" 中的 1.2
|
||||
const str = String(memStr); // 显式转换为字符串类型
|
||||
const match = str.match(/^([\d.]+)/);
|
||||
return match ? Number.parseFloat(match[1] as string) : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 渲染内存使用图表 */
|
||||
function renderMemoryChart() {
|
||||
if (!props.redisData?.info) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
const usedMemory = props.redisData.info.used_memory_human || '0';
|
||||
const memoryValue = parseMemoryValue(usedMemory);
|
||||
|
||||
// 渲染图表
|
||||
renderEcharts({
|
||||
title: {
|
||||
text: '内存使用情况',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
formatter: `{b} <br/>{a} : ${usedMemory}`,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '峰值',
|
||||
type: 'gauge',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
color: '#F5C74E',
|
||||
radius: '85%',
|
||||
center: ['50%', '50%'],
|
||||
startAngle: 225,
|
||||
endAngle: -45,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: [
|
||||
[0.2, '#7FFF00'],
|
||||
[0.8, '#00FFFF'],
|
||||
[1, '#FF0000'],
|
||||
],
|
||||
width: 10,
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
length: 5,
|
||||
lineStyle: {
|
||||
color: '#76D9D7',
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
length: 20,
|
||||
lineStyle: {
|
||||
color: '#76D9D7',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#76D9D7',
|
||||
distance: 15,
|
||||
fontSize: 15,
|
||||
},
|
||||
pointer: {
|
||||
width: 7,
|
||||
show: true,
|
||||
},
|
||||
detail: {
|
||||
show: true,
|
||||
offsetCenter: [0, '50%'],
|
||||
color: 'auto',
|
||||
fontSize: 30,
|
||||
formatter: usedMemory,
|
||||
},
|
||||
progress: {
|
||||
show: true,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: memoryValue,
|
||||
name: '内存消耗',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 监听数据变化,重新渲染图表 */
|
||||
watch(
|
||||
() => props.redisData,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
renderMemoryChart();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.redisData) {
|
||||
renderMemoryChart();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" height="420px" />
|
||||
</template>
|
||||
35
apps/web-naive/src/views/infra/server/index.vue
Normal file
35
apps/web-naive/src/views/infra/server/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, IFrame, Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
|
||||
const loading = ref(true); // 是否加载中
|
||||
const src = ref(`${import.meta.env.VITE_BASE_URL}/admin/applications`);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 友情提示:如果访问出现 404 问题:
|
||||
// 1)boot 参考 https://doc.iocoder.cn/server-monitor/ 解决;
|
||||
// 2)cloud 参考 https://cloud.iocoder.cn/server-monitor/ 解决
|
||||
const data = await getConfigKey('url.spring-boot-admin');
|
||||
if (data && data.length > 0) {
|
||||
src.value = data;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" />
|
||||
</template>
|
||||
|
||||
<IFrame v-if="!loading" v-loading="loading" :src="src" />
|
||||
</Page>
|
||||
</template>
|
||||
32
apps/web-naive/src/views/infra/skywalking/index.vue
Normal file
32
apps/web-naive/src/views/infra/skywalking/index.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, IFrame, Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
|
||||
const loading = ref(true); // 是否加载中
|
||||
const src = ref('http://skywalking.shop.iocoder.cn');
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await getConfigKey('url.skywalking');
|
||||
if (data && data.length > 0) {
|
||||
src.value = data;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" />
|
||||
</template>
|
||||
|
||||
<IFrame v-if="!loading" v-loading="loading" :src="src" />
|
||||
</Page>
|
||||
</template>
|
||||
33
apps/web-naive/src/views/infra/swagger/index.vue
Normal file
33
apps/web-naive/src/views/infra/swagger/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, IFrame, Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
|
||||
const loading = ref(true); // 是否加载中
|
||||
const src = ref(`${import.meta.env.VITE_BASE_URL}/doc.html`); // Knife4j UI
|
||||
// const src = ref(import.meta.env.VITE_BASE_URL + '/swagger-ui') // Swagger UI
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await getConfigKey('url.swagger');
|
||||
if (data && data.length > 0) {
|
||||
src.value = data;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="接口文档" url="https://doc.iocoder.cn/api-doc/" />
|
||||
</template>
|
||||
|
||||
<IFrame v-if="!loading" :src="src" />
|
||||
</Page>
|
||||
</template>
|
||||
316
apps/web-naive/src/views/infra/webSocket/index.vue
Normal file
316
apps/web-naive/src/views/infra/webSocket/index.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
|
||||
import { computed, onMounted, ref, watchEffect } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { useWebSocket } from '@vueuse/core';
|
||||
import {
|
||||
NBadge,
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NEmpty,
|
||||
NInput,
|
||||
NSelect,
|
||||
NTag,
|
||||
} from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const refreshToken = accessStore.refreshToken as string;
|
||||
|
||||
const server = ref(
|
||||
`${`${import.meta.env.VITE_BASE_URL}/infra/ws`.replace(
|
||||
'http',
|
||||
'ws',
|
||||
)}?token=${refreshToken}`, // 使用 refreshToken,而不使用 accessToken 方法的原因:WebSocket 无法方便的刷新访问令牌
|
||||
); // WebSocket 服务地址
|
||||
const getIsOpen = computed(() => status.value === 'OPEN'); // WebSocket 连接是否打开
|
||||
const getTagColor = computed(() =>
|
||||
getIsOpen.value ? { color: 'green' } : { color: 'red' },
|
||||
); // WebSocket 连接的展示颜色
|
||||
const getStatusText = computed(() => (getIsOpen.value ? '已连接' : '未连接')); // 连接状态文本
|
||||
|
||||
/** 发起 WebSocket 连接 */
|
||||
const { status, data, send, close, open } = useWebSocket(server.value, {
|
||||
autoReconnect: true,
|
||||
heartbeat: true,
|
||||
});
|
||||
|
||||
/** 监听接收到的数据 */
|
||||
const messageList = ref(
|
||||
[] as { text: string; time: number; type?: string; userId?: string }[],
|
||||
); // 消息列表
|
||||
const messageReverseList = computed(() => [...messageList.value].reverse());
|
||||
watchEffect(() => {
|
||||
if (!data.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 1. 收到心跳
|
||||
if (data.value === 'pong') {
|
||||
// state.recordList.push({
|
||||
// text: '【心跳】',
|
||||
// time: new Date().getTime()
|
||||
// })
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析 type 消息类型
|
||||
const jsonMessage = JSON.parse(data.value);
|
||||
const type = jsonMessage.type;
|
||||
const content = JSON.parse(jsonMessage.content);
|
||||
if (!type) {
|
||||
message.error(`未知的消息类型:${data.value}`);
|
||||
return;
|
||||
}
|
||||
// 2.2 消息类型:demo-message-receive
|
||||
if (type === 'demo-message-receive') {
|
||||
const single = content.single;
|
||||
messageList.value.push({
|
||||
text: content.text,
|
||||
time: Date.now(),
|
||||
type: single ? 'single' : 'group',
|
||||
userId: content.fromUserId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 2.3 消息类型:notice-push
|
||||
if (type === 'notice-push') {
|
||||
messageList.value.push({
|
||||
text: content.title,
|
||||
time: Date.now(),
|
||||
type: 'system',
|
||||
});
|
||||
return;
|
||||
}
|
||||
message.error(`未处理消息:${data.value}`);
|
||||
} catch (error) {
|
||||
message.error(`处理消息发生异常:${data.value}`);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
/** 发送消息 */
|
||||
const sendText = ref(''); // 发送内容
|
||||
const sendUserId = ref('all'); // 发送人
|
||||
function handlerSend() {
|
||||
if (!sendText.value.trim()) {
|
||||
message.warning('消息内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 先 JSON 化 message 消息内容
|
||||
const messageContent = JSON.stringify({
|
||||
text: sendText.value,
|
||||
toUserId: sendUserId.value === 'all' ? undefined : sendUserId.value,
|
||||
});
|
||||
// 1.2 再 JSON 化整个消息
|
||||
const jsonMessage = JSON.stringify({
|
||||
type: 'demo-message-send',
|
||||
content: messageContent,
|
||||
});
|
||||
// 2. 最后发送消息
|
||||
send(jsonMessage);
|
||||
sendText.value = '';
|
||||
}
|
||||
|
||||
/** 切换 websocket 连接状态 */
|
||||
function toggleConnectStatus() {
|
||||
if (getIsOpen.value) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取消息类型的徽标颜色 */
|
||||
function getMessageBadgeColor(type?: string) {
|
||||
switch (type) {
|
||||
case 'group': {
|
||||
return 'green';
|
||||
}
|
||||
case 'single': {
|
||||
return 'blue';
|
||||
}
|
||||
case 'system': {
|
||||
return 'red';
|
||||
}
|
||||
default: {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取消息类型的文本 */
|
||||
function getMessageTypeText(type?: string) {
|
||||
switch (type) {
|
||||
case 'group': {
|
||||
return '群发';
|
||||
}
|
||||
case 'single': {
|
||||
return '单发';
|
||||
}
|
||||
case 'system': {
|
||||
return '系统';
|
||||
}
|
||||
default: {
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
const userList = ref<SelectOption[]>([{ label: '所有人', value: '' }]); // 用户列表
|
||||
|
||||
async function initUserOptions() {
|
||||
const res = await getSimpleUserList();
|
||||
userList.value = res.map((item) => {
|
||||
return {
|
||||
label: item.nickname,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
onMounted(async () => {
|
||||
await initUserOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="WebSocket 实时通信"
|
||||
url="https://doc.iocoder.cn/websocket/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-4 md:flex-row">
|
||||
<!-- 左侧:建立连接、发送消息 -->
|
||||
<NCard :bordered="false" class="w-full md:w-1/2">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<NBadge :status="getIsOpen ? 'success' : 'error'" />
|
||||
<span class="ml-2 text-lg font-medium">连接管理</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mb-4 flex items-center rounded-lg p-3">
|
||||
<span class="mr-4 font-medium">连接状态:</span>
|
||||
<NTag :color="getTagColor" class="px-3 py-1">
|
||||
{{ getStatusText }}
|
||||
</NTag>
|
||||
</div>
|
||||
<div class="mb-6 flex space-x-2">
|
||||
<NInput
|
||||
v-model:value="server"
|
||||
disabled
|
||||
class="rounded-md"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="text-gray-600">服务地址</span>
|
||||
</template>
|
||||
</NInput>
|
||||
<NButton
|
||||
:type="getIsOpen ? 'default' : 'primary'"
|
||||
:danger="getIsOpen"
|
||||
size="large"
|
||||
class="flex-shrink-0"
|
||||
@click="toggleConnectStatus"
|
||||
>
|
||||
{{ getIsOpen ? '关闭连接' : '开启连接' }}
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<NDivider>
|
||||
<span class="text-gray-500">消息发送</span>
|
||||
</NDivider>
|
||||
|
||||
<NSelect
|
||||
v-model:value="sendUserId"
|
||||
class="mb-3 w-full"
|
||||
size="large"
|
||||
placeholder="请选择接收人"
|
||||
:disabled="!getIsOpen"
|
||||
:options="userList"
|
||||
/>
|
||||
|
||||
<NInput
|
||||
v-model:value="sendText"
|
||||
type="textarea"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:disabled="!getIsOpen"
|
||||
class="border-1 rounded-lg"
|
||||
allow-clear
|
||||
placeholder="请输入你要发送的消息..."
|
||||
/>
|
||||
|
||||
<NButton
|
||||
:disabled="!getIsOpen"
|
||||
block
|
||||
class="mt-4"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handlerSend"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:send-horizontal" />
|
||||
</template>
|
||||
发送消息
|
||||
</NButton>
|
||||
</NCard>
|
||||
|
||||
<!-- 右侧:消息记录 -->
|
||||
<NCard :bordered="false" class="w-full md:w-1/2">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon
|
||||
icon="lucide:message-circle-more"
|
||||
class="mr-2 text-lg"
|
||||
/>
|
||||
<span class="text-lg font-medium">消息记录</span>
|
||||
<NTag v-if="messageList.length > 0" class="ml-2">
|
||||
{{ messageList.length }} 条
|
||||
</NTag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-96 overflow-auto rounded-lg p-2">
|
||||
<NEmpty v-if="messageList.length === 0" description="暂无消息记录" />
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="msg in messageReverseList"
|
||||
:key="msg.time"
|
||||
class="rounded-lg p-3 shadow-sm"
|
||||
>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<NBadge :color="getMessageBadgeColor(msg.type)" />
|
||||
<span class="ml-1 font-medium text-gray-600">
|
||||
{{ getMessageTypeText(msg.type) }}
|
||||
</span>
|
||||
<span v-if="msg.userId" class="ml-2 text-gray-500">
|
||||
用户 ID: {{ msg.userId }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ formatDate(msg.time) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 break-words text-gray-800">
|
||||
{{ msg.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
Reference in New Issue
Block a user