feat: 初步适配naive 未测试
parent
d59c137036
commit
695524c37f
|
|
@ -0,0 +1,173 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
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: 'RangePicker',
|
||||
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<T = InfraApiAccessLogApi.ApiAccessLog>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): 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: ({ row }) => `${row.duration} 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 },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'id',
|
||||
nameTitle: 'API访问日志',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['infra:api-access-log:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
exportApiAccessLog,
|
||||
getApiAccessLogPage,
|
||||
} from '#/api/infra/api-access-log';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportApiAccessLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: 'API 访问日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看 API 访问日志详情 */
|
||||
function onDetail(row: InfraApiAccessLogApi.ApiAccessLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraApiAccessLogApi.ApiAccessLog>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getApiAccessLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
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="onRefresh" />
|
||||
<Grid table-title="API 访问日志列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['infra:api-access-log:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
|
||||
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"
|
||||
>
|
||||
<NDescriptions
|
||||
bordered
|
||||
:column="1"
|
||||
size="middle"
|
||||
class="mx-4"
|
||||
:label-style="{ width: '110px' }"
|
||||
>
|
||||
<NDescriptionsItem label="日志编号">
|
||||
{{ formData?.id }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="链路追踪">
|
||||
{{ formData?.traceId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="应用名">
|
||||
{{ formData?.applicationName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户信息">
|
||||
{{ formData?.userId }}
|
||||
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户IP">
|
||||
{{ formData?.userIp }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户UA">
|
||||
{{ formData?.userAgent }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求信息">
|
||||
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求参数">
|
||||
{{ formData?.requestParams }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求结果">
|
||||
{{ formData?.responseBody }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求时间">
|
||||
{{ formatDateTime(formData?.beginTime || '') }} ~
|
||||
{{ formatDateTime(formData?.endTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求耗时">
|
||||
{{ formData?.duration }} ms
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作结果">
|
||||
<div v-if="formData?.resultCode === 0">正常</div>
|
||||
<div v-else-if="formData && formData?.resultCode > 0">
|
||||
失败 | {{ formData?.resultCode }} | {{ formData?.resultMsg }}
|
||||
</div>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作模块">
|
||||
{{ formData?.operateModule }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作名">
|
||||
{{ formData?.operateName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.INFRA_OPERATE_TYPE"
|
||||
:value="formData?.operateType"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import {
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
InfraApiErrorLogProcessStatusEnum,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
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: 'RangePicker',
|
||||
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<T = InfraApiErrorLogApi.ApiErrorLog>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): 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 },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 200,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'id',
|
||||
nameTitle: 'API错误日志',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['infra:api-error-log:query']),
|
||||
},
|
||||
{
|
||||
code: 'done',
|
||||
text: '已处理',
|
||||
show: (row: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return (
|
||||
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT &&
|
||||
hasAccessByCodes(['infra:api-error-log:update-status'])
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
code: 'ignore',
|
||||
text: '已忽略',
|
||||
show: (row: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return (
|
||||
row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT &&
|
||||
hasAccessByCodes(['infra:api-error-log:update-status'])
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
exportApiErrorLog,
|
||||
getApiErrorLogPage,
|
||||
updateApiErrorLogStatus,
|
||||
} from '#/api/infra/api-error-log';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
import { InfraApiErrorLogProcessStatusEnum } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportApiErrorLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: 'API 错误日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看 API 错误日志详情 */
|
||||
function onDetail(row: InfraApiErrorLogApi.ApiErrorLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 处理已处理 / 已忽略的操作 */
|
||||
async function onProcess(id: number, processStatus: number) {
|
||||
confirm({
|
||||
content: `确认标记为${InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'}?`,
|
||||
}).then(async () => {
|
||||
await updateApiErrorLogStatus(id, processStatus);
|
||||
// 关闭并提示
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraApiErrorLogApi.ApiErrorLog>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
case 'done': {
|
||||
onProcess(row.id, InfraApiErrorLogProcessStatusEnum.DONE);
|
||||
break;
|
||||
}
|
||||
case 'ignore': {
|
||||
onProcess(row.id, InfraApiErrorLogProcessStatusEnum.IGNORE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getApiErrorLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
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="onRefresh" />
|
||||
<Grid table-title="API 错误日志列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['infra:api-error-log:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem, NInput } from 'naive-ui';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
|
||||
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"
|
||||
>
|
||||
<NDescriptions
|
||||
bordered
|
||||
:column="1"
|
||||
size="middle"
|
||||
class="mx-4"
|
||||
:label-style="{ width: '110px' }"
|
||||
>
|
||||
<NDescriptionsItem label="日志编号">
|
||||
{{ formData?.id }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="链路追踪">
|
||||
{{ formData?.traceId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="应用名">
|
||||
{{ formData?.applicationName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户编号">
|
||||
{{ formData?.userId }}
|
||||
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户IP">
|
||||
{{ formData?.userIp }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户UA">
|
||||
{{ formData?.userAgent }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求信息">
|
||||
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求参数">
|
||||
{{ formData?.requestParams }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="异常时间">
|
||||
{{ formatDateTime(formData?.exceptionTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="异常名">
|
||||
{{ formData?.exceptionName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="formData?.exceptionStackTrace" label="异常堆栈">
|
||||
<NInput
|
||||
type="textarea"
|
||||
:value="formData?.exceptionStackTrace"
|
||||
:auto-size="{ maxRows: 20 }"
|
||||
readonly
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="处理状态">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
|
||||
:value="formData?.processStatus"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="formData?.processUserId" label="处理人">
|
||||
{{ formData?.processUserId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="formData?.processTime" label="处理时间">
|
||||
{{ formatDateTime(formData?.processTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<!-- eslint-disable no-useless-escape -->
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, unref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { isString } from '@vben/utils';
|
||||
|
||||
import formCreate from '@form-create/ant-design-vue';
|
||||
import FcDesigner from '@form-create/antd-designer';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import hljs from 'highlight.js';
|
||||
import xml from 'highlight.js/lib/languages/java';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useFormCreateDesigner } from '#/components/form-create';
|
||||
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
defineOptions({ name: 'InfraBuild' });
|
||||
|
||||
const [Modal, modalApi] = useVbenModal();
|
||||
|
||||
const designer = ref(); // 表单设计器
|
||||
|
||||
// 表单设计器配置
|
||||
const designerConfig = ref({
|
||||
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
|
||||
autoActive: true, // 是否自动选中拖入的组件
|
||||
useTemplate: false, // 是否生成vue2语法的模板组件
|
||||
formOptions: {
|
||||
form: {
|
||||
labelWidth: '100px', // 设置默认的 label 宽度为 100px
|
||||
},
|
||||
}, // 定义表单配置默认值
|
||||
fieldReadonly: false, // 配置field是否可以编辑
|
||||
hiddenDragMenu: false, // 隐藏拖拽操作按钮
|
||||
hiddenDragBtn: false, // 隐藏拖拽按钮
|
||||
hiddenMenu: [], // 隐藏部分菜单
|
||||
hiddenItem: [], // 隐藏部分组件
|
||||
hiddenItemConfig: {}, // 隐藏组件的部分配置项
|
||||
disabledItemConfig: {}, // 禁用组件的部分配置项
|
||||
showSaveBtn: false, // 是否显示保存按钮
|
||||
showConfig: true, // 是否显示右侧的配置界面
|
||||
showBaseForm: true, // 是否显示组件的基础配置表单
|
||||
showControl: true, // 是否显示组件联动
|
||||
showPropsForm: true, // 是否显示组件的属性配置表单
|
||||
showEventForm: true, // 是否显示组件的事件配置表单
|
||||
showValidateForm: true, // 是否显示组件的验证配置表单
|
||||
showFormConfig: true, // 是否显示表单配置
|
||||
showInputData: true, // 是否显示录入按钮
|
||||
showDevice: true, // 是否显示多端适配选项
|
||||
appendConfigData: [], // 定义渲染规则所需的formData
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const dialogTitle = ref(''); // 弹窗的标题
|
||||
const formType = ref(-1); // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件
|
||||
const formData = ref(''); // 表单数据
|
||||
useFormCreateDesigner(designer); // 表单设计器增强
|
||||
|
||||
/** 打开弹窗 */
|
||||
const openModel = (title: string) => {
|
||||
dialogVisible.value = true;
|
||||
dialogTitle.value = title;
|
||||
modalApi.open();
|
||||
};
|
||||
|
||||
/** 生成 JSON */
|
||||
const showJson = () => {
|
||||
openModel('生成 JSON');
|
||||
formType.value = 0;
|
||||
formData.value = designer.value.getRule();
|
||||
};
|
||||
|
||||
/** 生成 Options */
|
||||
const showOption = () => {
|
||||
openModel('生成 Options');
|
||||
formType.value = 1;
|
||||
formData.value = designer.value.getOption();
|
||||
};
|
||||
|
||||
/** 生成组件 */
|
||||
const showTemplate = () => {
|
||||
openModel('生成组件');
|
||||
formType.value = 2;
|
||||
formData.value = makeTemplate();
|
||||
};
|
||||
|
||||
const makeTemplate = () => {
|
||||
const rule = designer.value.getRule();
|
||||
const opt = designer.value.getOption();
|
||||
return `<template>
|
||||
<form-create
|
||||
v-model:api="fApi"
|
||||
:rule="rule"
|
||||
:option="option"
|
||||
@submit="onSubmit"
|
||||
></form-create>
|
||||
</template>
|
||||
<script setup lang=ts>
|
||||
const faps = ref(null)
|
||||
const rule = ref('')
|
||||
const option = ref('')
|
||||
const init = () => {
|
||||
rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
|
||||
option.value = formCreate.parseJson('${JSON.stringify(opt, null, 2)}')
|
||||
}
|
||||
const onSubmit = (formData) => {
|
||||
//todo 提交表单
|
||||
}
|
||||
init()
|
||||
<\/script>`;
|
||||
};
|
||||
|
||||
/** 复制 */
|
||||
const copy = async (text: string) => {
|
||||
const textToCopy = JSON.stringify(text, null, 2);
|
||||
const { copy, copied, isSupported } = useClipboard({ source: textToCopy });
|
||||
if (isSupported) {
|
||||
await copy();
|
||||
if (unref(copied)) {
|
||||
message.success('复制成功');
|
||||
}
|
||||
} else {
|
||||
message.error('复制失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 代码高亮
|
||||
*/
|
||||
const highlightedCode = (code: string) => {
|
||||
// 处理语言和代码
|
||||
let language = 'json';
|
||||
if (formType.value === 2) {
|
||||
language = 'xml';
|
||||
}
|
||||
// debugger
|
||||
if (!isString(code)) {
|
||||
code = JSON.stringify(code, null, 2);
|
||||
}
|
||||
// 高亮
|
||||
const result = hljs.highlight(code, { language, ignoreIllegals: true });
|
||||
return result.value || ' ';
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 注册代码高亮的各种语言
|
||||
hljs.registerLanguage('xml', xml);
|
||||
hljs.registerLanguage('json', json);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FcDesigner ref="designer" height="90vh" :config="designerConfig">
|
||||
<template #handle>
|
||||
<NButton size="small" type="primary" ghost @click="showJson">
|
||||
生成JSON
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" ghost @click="showOption">
|
||||
生成Options
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" ghost @click="showTemplate">
|
||||
生成组件
|
||||
</NButton>
|
||||
</template>
|
||||
</FcDesigner>
|
||||
|
||||
<!-- 弹窗:表单预览 -->
|
||||
<Modal :title="dialogTitle" :footer="false" :fullscreen-button="false">
|
||||
<div>
|
||||
<NButton style="float: right" @click="copy(formData)"> 复制 </NButton>
|
||||
<div>
|
||||
<pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
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 { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 导入数据库表的表单 */
|
||||
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: 'Textarea',
|
||||
componentProps: {
|
||||
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: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = InfraCodegenApi.CodegenTable>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
getDataSourceConfigName?: (dataSourceConfigId: number) => string | undefined,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'dataSourceConfigId',
|
||||
title: '数据源',
|
||||
minWidth: 120,
|
||||
formatter: (row) => getDataSourceConfigName?.(row.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',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 300,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'tableName',
|
||||
nameTitle: '代码生成',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'preview',
|
||||
text: '预览',
|
||||
show: hasAccessByCodes(['infra:codegen:preview']),
|
||||
},
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['infra:codegen:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['infra:codegen:delete']),
|
||||
},
|
||||
{
|
||||
code: 'sync',
|
||||
text: '同步',
|
||||
show: hasAccessByCodes(['infra:codegen:update']),
|
||||
},
|
||||
{
|
||||
code: 'generate',
|
||||
text: '生成代码',
|
||||
show: hasAccessByCodes(['infra:codegen:download']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 代码生成表格列定义 */
|
||||
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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<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, 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(0);
|
||||
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>>();
|
||||
|
||||
/** 获取详情数据 */
|
||||
const getDetail = async () => {
|
||||
const id = route.query.id as any;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
formData.value = await getCodegenTable(id);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = async () => {
|
||||
// 表单验证
|
||||
const basicInfoValid = await basicInfoRef.value?.validate();
|
||||
if (!basicInfoValid) {
|
||||
message.warn('保存失败,原因:基本信息表单校验失败请检查!!!');
|
||||
return;
|
||||
}
|
||||
const generateInfoValid = await generateInfoRef.value?.validate();
|
||||
if (!generateInfoValid) {
|
||||
message.warn('保存失败,原因:生成信息表单校验失败请检查!!!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.updating'),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
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();
|
||||
}
|
||||
};
|
||||
const tabs = useTabs();
|
||||
/** 返回列表 */
|
||||
const close = () => {
|
||||
tabs.closeCurrentTab();
|
||||
router.push('/infra/codegen');
|
||||
};
|
||||
|
||||
/** 下一步 */
|
||||
const nextStep = async () => {
|
||||
currentStep.value += 1;
|
||||
};
|
||||
|
||||
/** 上一步 */
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value -= 1;
|
||||
}
|
||||
};
|
||||
|
||||
/** 步骤配置 */
|
||||
const steps = [
|
||||
{
|
||||
title: '基本信息',
|
||||
},
|
||||
{
|
||||
title: '字段信息',
|
||||
},
|
||||
{
|
||||
title: '生成信息',
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化
|
||||
getDetail();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<div
|
||||
class="flex h-[95%] flex-col rounded-md bg-white p-4 dark:bg-[#1f1f1f] dark:text-gray-300"
|
||||
>
|
||||
<NSteps
|
||||
type="navigation"
|
||||
v-model:current="currentStep"
|
||||
class="mb-8 rounded shadow-sm dark:bg-[#141414]"
|
||||
>
|
||||
<Steps.Step
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
:title="step.title"
|
||||
/>
|
||||
</NSteps>
|
||||
|
||||
<div class="flex-1 overflow-auto py-4">
|
||||
<!-- 根据当前步骤显示对应的组件 -->
|
||||
<BasicInfo
|
||||
v-show="currentStep === 0"
|
||||
ref="basicInfoRef"
|
||||
:table="formData.table"
|
||||
/>
|
||||
<ColumnInfo
|
||||
v-show="currentStep === 1"
|
||||
ref="columnInfoRef"
|
||||
:columns="formData.columns"
|
||||
/>
|
||||
<GenerationInfo
|
||||
v-show="currentStep === 2"
|
||||
ref="generateInfoRef"
|
||||
:table="formData.table"
|
||||
:columns="formData.columns"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<NButton v-show="currentStep > 0" @click="prevStep">上一步</NButton>
|
||||
<NButton v-show="currentStep < steps.length - 1" @click="nextStep">
|
||||
下一步
|
||||
</NButton>
|
||||
<NButton type="primary" :loading="loading" @click="submitForm">
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
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 { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCodegenTable,
|
||||
downloadCodegen,
|
||||
getCodegenTablePage,
|
||||
syncCodegenFromDB,
|
||||
} from '#/api/infra/codegen';
|
||||
import { getDataSourceConfigList } from '#/api/infra/data-source-config';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
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 onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导入表格 */
|
||||
function onImport() {
|
||||
importModalApi.open();
|
||||
}
|
||||
|
||||
/** 预览代码 */
|
||||
function onPreview(row: InfraCodegenApi.CodegenTable) {
|
||||
previewModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 编辑表格 */
|
||||
function onEdit(row: InfraCodegenApi.CodegenTable) {
|
||||
router.push({ name: 'InfraCodegenEdit', query: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 删除代码生成配置 */
|
||||
async function onDelete(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.tableName]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteCodegenTable(row.id);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.tableName]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 同步数据库 */
|
||||
async function onSync(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.updating', [row.tableName]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await syncCodegenFromDB(row.id);
|
||||
message.success($t('ui.actionMessage.updateSuccess', [row.tableName]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成代码 */
|
||||
async function onGenerate(row: InfraCodegenApi.CodegenTable) {
|
||||
const hideLoading = message.loading({
|
||||
content: '正在生成代码...',
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraCodegenApi.CodegenTable>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'generate': {
|
||||
onGenerate(row);
|
||||
break;
|
||||
}
|
||||
case 'preview': {
|
||||
onPreview(row);
|
||||
break;
|
||||
}
|
||||
case 'sync': {
|
||||
onSync(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick, getDataSourceConfigName),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCodegenTablePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraCodegenApi.CodegenTable>,
|
||||
});
|
||||
|
||||
/** 获取数据源配置列表 */
|
||||
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="onRefresh" />
|
||||
<PreviewModal />
|
||||
<Grid table-title="代码生成列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onImport"
|
||||
v-access:code="['infra:codegen:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
导入
|
||||
</NButton>
|
||||
</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>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict/type';
|
||||
|
||||
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',
|
||||
},
|
||||
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<SystemDictTypeApi.DictType[]>([]); // 字典类型选项
|
||||
onMounted(async () => {
|
||||
dictTypeOptions.value = await getSimpleDictTypeList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid>
|
||||
<!-- 字段描述 -->
|
||||
<template #columnComment="{ row }">
|
||||
<NInput v-model:value="row.columnComment" />
|
||||
</template>
|
||||
|
||||
<!-- Java 类型 -->
|
||||
<template #javaType="{ row, column }">
|
||||
<NSelect v-model:value="row.javaType" style="width: 100%">
|
||||
<NSelect.Option
|
||||
v-for="option in column.params.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</NSelect.Option>
|
||||
</NSelect>
|
||||
</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" class="w-full">
|
||||
<NSelect.Option
|
||||
v-for="option in column.params.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</NSelect.Option>
|
||||
</NSelect>
|
||||
</template>
|
||||
|
||||
<!-- 允许空 -->
|
||||
<template #nullable="{ row }">
|
||||
<NCheckbox v-model:checked="row.nullable" />
|
||||
</template>
|
||||
|
||||
<!-- 显示类型 -->
|
||||
<template #htmlType="{ row, column }">
|
||||
<NSelect v-model:value="row.htmlType" class="w-full">
|
||||
<NSelect.Option
|
||||
v-for="option in column.params.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</NSelect.Option>
|
||||
</NSelect>
|
||||
</template>
|
||||
|
||||
<!-- 字典类型 -->
|
||||
<template #dictType="{ row }">
|
||||
<NSelect
|
||||
v-model:value="row.dictType"
|
||||
class="w-full"
|
||||
allow-clear
|
||||
show-search
|
||||
>
|
||||
>
|
||||
<NSelect.Option
|
||||
v-for="option in dictTypeOptions"
|
||||
:key="option.type"
|
||||
:value="option.type"
|
||||
>
|
||||
{{ option.name }}
|
||||
</NSelect.Option>
|
||||
</NSelect>
|
||||
</template>
|
||||
|
||||
<!-- 示例 -->
|
||||
<template #example="{ row }">
|
||||
<NInput v-model:value="row.example" />
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getCodegenTableList } from '#/api/infra/codegen';
|
||||
import { InfraCodegenTemplateTypeEnum } from '#/utils';
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新主子表信息表单 schema */
|
||||
function updateSubSchema(): void {
|
||||
subFormApi.setState({
|
||||
schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value),
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取合并的表单值 */
|
||||
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>
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<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',
|
||||
},
|
||||
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({
|
||||
content: '导入中...',
|
||||
duration: 0,
|
||||
key: 'import_loading',
|
||||
});
|
||||
try {
|
||||
await createCodegenList(formData);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
hideLoading();
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal>
|
||||
<Grid />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
<script lang="ts" setup>
|
||||
// TODO @芋艿:待定,vben2.0 有 CodeEditor,不确定官方后续会不会迁移!!!
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Copy } from '@vben/icons';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import java from 'highlight.js/lib/languages/java';
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import sql from 'highlight.js/lib/languages/sql';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import xml from 'highlight.js/lib/languages/xml';
|
||||
import { NButton, NTabs, NTree } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { previewCodegen } from '#/api/infra/codegen';
|
||||
|
||||
/** 注册代码高亮语言 */
|
||||
hljs.registerLanguage('java', java);
|
||||
hljs.registerLanguage('xml', xml);
|
||||
hljs.registerLanguage('html', xml);
|
||||
hljs.registerLanguage('vue', xml);
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('sql', sql);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
|
||||
/** 文件树类型 */
|
||||
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>());
|
||||
const setCodeMap = (key: string, lang: string, code: string) => {
|
||||
// 处理可能的缩进问题,特别是对Java文件
|
||||
const trimmedCode = code.trimStart();
|
||||
// 如果已有缓存则不重新构建
|
||||
if (codeMap.value.has(key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const highlightedCode = hljs.highlight(trimmedCode, {
|
||||
language: lang,
|
||||
}).value;
|
||||
codeMap.value.set(key, highlightedCode);
|
||||
} catch {
|
||||
codeMap.value.set(key, trimmedCode);
|
||||
}
|
||||
};
|
||||
const removeCodeMapKey = (targetKey: any) => {
|
||||
// 只有一个代码视图时不允许删除
|
||||
if (codeMap.value.size === 1) {
|
||||
return;
|
||||
}
|
||||
if (codeMap.value.has(targetKey)) {
|
||||
codeMap.value.delete(targetKey);
|
||||
}
|
||||
};
|
||||
|
||||
/** 复制代码 */
|
||||
const copyCode = async () => {
|
||||
const { copy } = useClipboard();
|
||||
const file = previewFiles.value.find(
|
||||
(item) => item.filePath === activeKey.value,
|
||||
);
|
||||
if (file) {
|
||||
await copy(file.code);
|
||||
message.success('复制成功');
|
||||
}
|
||||
};
|
||||
|
||||
/** 文件节点点击事件 */
|
||||
const handleNodeClick = (_: any[], e: any) => {
|
||||
if (!e.node.isLeaf) return;
|
||||
|
||||
activeKey.value = e.node.key;
|
||||
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;
|
||||
|
||||
const lang = file.filePath.split('.').pop() || '';
|
||||
setCodeMap(activeKey.value, lang, file.code);
|
||||
};
|
||||
|
||||
/** 处理文件树 */
|
||||
const 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++;
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建树形结构 */
|
||||
const 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 lang = activeKey.value.split('.').pop() || '';
|
||||
const code = data[0]?.code || '';
|
||||
setCodeMap(activeKey.value, lang, 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"
|
||||
default-expand-all
|
||||
v-model:active-key="activeKey"
|
||||
@select="handleNodeClick"
|
||||
:tree-data="fileTree"
|
||||
/>
|
||||
</div>
|
||||
<!-- 代码预览 -->
|
||||
<div class="h-full w-2/3 overflow-auto pl-4">
|
||||
<NTabs
|
||||
v-model:active-key="activeKey"
|
||||
hide-add
|
||||
type="editable-card"
|
||||
@edit="removeCodeMapKey"
|
||||
>
|
||||
<NTabPane
|
||||
v-for="key in codeMap.keys()"
|
||||
: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"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<code
|
||||
v-html="codeMap.get(activeKey)"
|
||||
class="code-highlight"
|
||||
></code>
|
||||
</div>
|
||||
</NTabPane>
|
||||
<template #rightExtra>
|
||||
<NButton type="primary" ghost @click="copyCode" :icon="h(Copy)">
|
||||
复制代码
|
||||
</NButton>
|
||||
</template>
|
||||
</NTabs>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
|
||||
/* 代码高亮样式 - 支持暗黑模式 */
|
||||
:deep(.code-highlight) {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 关键字 */
|
||||
:deep(.hljs-keyword) {
|
||||
@apply text-purple-600 dark:text-purple-400;
|
||||
}
|
||||
|
||||
/* 字符串 */
|
||||
:deep(.hljs-string) {
|
||||
@apply text-green-600 dark:text-green-400;
|
||||
}
|
||||
|
||||
/* 注释 */
|
||||
:deep(.hljs-comment) {
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* 函数 */
|
||||
:deep(.hljs-function) {
|
||||
@apply text-blue-600 dark:text-blue-400;
|
||||
}
|
||||
|
||||
/* 数字 */
|
||||
:deep(.hljs-number) {
|
||||
@apply text-orange-600 dark:text-orange-400;
|
||||
}
|
||||
|
||||
/* 类 */
|
||||
:deep(.hljs-class) {
|
||||
@apply text-yellow-600 dark:text-yellow-400;
|
||||
}
|
||||
|
||||
/* 标题/函数名 */
|
||||
:deep(.hljs-title) {
|
||||
@apply font-bold text-blue-600 dark:text-blue-400;
|
||||
}
|
||||
|
||||
/* 参数 */
|
||||
:deep(.hljs-params) {
|
||||
@apply text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
/* 内置对象 */
|
||||
:deep(.hljs-built_in) {
|
||||
@apply text-teal-600 dark:text-teal-400;
|
||||
}
|
||||
|
||||
/* HTML标签 */
|
||||
:deep(.hljs-tag) {
|
||||
@apply text-blue-600 dark:text-blue-400;
|
||||
}
|
||||
|
||||
/* 属性 */
|
||||
:deep(.hljs-attribute),
|
||||
:deep(.hljs-attr) {
|
||||
@apply text-green-600 dark:text-green-400;
|
||||
}
|
||||
|
||||
/* 字面量 */
|
||||
:deep(.hljs-literal) {
|
||||
@apply text-purple-600 dark:text-purple-400;
|
||||
}
|
||||
|
||||
/* 元信息 */
|
||||
:deep(.hljs-meta) {
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* 选择器标签 */
|
||||
:deep(.hljs-selector-tag) {
|
||||
@apply text-blue-600 dark:text-blue-400;
|
||||
}
|
||||
|
||||
/* XML/HTML名称 */
|
||||
:deep(.hljs-name) {
|
||||
@apply text-blue-600 dark:text-blue-400;
|
||||
}
|
||||
|
||||
/* 变量 */
|
||||
:deep(.hljs-variable) {
|
||||
@apply text-orange-600 dark:text-orange-400;
|
||||
}
|
||||
|
||||
/* 属性 */
|
||||
:deep(.hljs-property) {
|
||||
@apply text-red-600 dark:text-red-400;
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraConfigApi } from '#/api/infra/config';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
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: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '参数名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'key',
|
||||
label: '参数键名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入参数键名',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '系统内置',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE, 'number'),
|
||||
placeholder: '请选择系统内置',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = InfraConfigApi.Config>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '参数',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['infra:config:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['infra:config:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraConfigApi } from '#/api/infra/config';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteConfig, 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 onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportConfig(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '参数配置.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建参数 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑参数 */
|
||||
function onEdit(row: InfraConfigApi.Config) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除参数 */
|
||||
async function onDelete(row: InfraConfigApi.Config) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteConfig(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraConfigApi.Config>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraConfigApi.Config>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="参数列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['infra:config:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['参数']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['infra:config:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -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 as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
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<T = InfraDataSourceConfigApi.DataSourceConfig>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '数据源',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['infra:data-source-config:update']),
|
||||
disabled: (row: any) => row.id === 0,
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['infra:data-source-config:delete']),
|
||||
disabled: (row: any) => row.id === 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDataSourceConfig,
|
||||
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 onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑数据源 */
|
||||
function onEdit(row: InfraDataSourceConfigApi.DataSourceConfig) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除数据源 */
|
||||
async function onDelete(row: InfraDataSourceConfigApi.DataSourceConfig) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteDataSourceConfig(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
await handleLoadData();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraDataSourceConfigApi.DataSourceConfig>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: getDataSourceConfigList,
|
||||
},
|
||||
},
|
||||
} as VxeTableGridOptions<InfraDataSourceConfigApi.DataSourceConfig>,
|
||||
});
|
||||
|
||||
/** 加载数据 */
|
||||
async function handleLoadData() {
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
async function onRefresh() {
|
||||
await handleLoadData();
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
handleLoadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="数据源列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['infra:data-source-config:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['数据源']) }}
|
||||
</NButton>
|
||||
</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 as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { IFrame } from '#/components/iframe';
|
||||
|
||||
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>
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraFileApi } from '#/api/infra/file';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 表单的字段 */
|
||||
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: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = InfraFileApi.File>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
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 @芋艿:后续优化下
|
||||
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',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '文件',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'copyUrl',
|
||||
text: '复制链接',
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['infra:file:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraFileApi } from '#/api/infra/file';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Upload } from '@vben/icons';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { NButton, NImage } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteFile, 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 onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
function onUpload() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 复制链接到剪贴板 */
|
||||
const { copy } = useClipboard({ legacy: true });
|
||||
async function onCopyUrl(row: InfraFileApi.File) {
|
||||
if (!row.url) {
|
||||
message.error('文件 URL 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copy(row.url);
|
||||
message.success('复制成功');
|
||||
} catch {
|
||||
message.error('复制失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开 URL */
|
||||
function openUrl(url?: string) {
|
||||
if (url) {
|
||||
openWindow(url);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
async function onDelete(row: InfraFileApi.File) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name || row.path]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteFile(row.id as number);
|
||||
message.success(
|
||||
$t('ui.actionMessage.deleteSuccess', [row.name || row.path]),
|
||||
);
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<InfraFileApi.File>) {
|
||||
switch (code) {
|
||||
case 'copyUrl': {
|
||||
onCopyUrl(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getFilePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraFileApi.File>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="文件列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton type="primary" @click="onUpload">
|
||||
<Upload class="size-5" />
|
||||
上传图片
|
||||
</NButton>
|
||||
</template>
|
||||
<template #file-content="{ row }">
|
||||
<NImage v-if="row.type && row.type.includes('image')" :src="row.url" />
|
||||
<NButton
|
||||
v-else-if="row.type && row.type.includes('pdf')"
|
||||
type="link"
|
||||
@click="() => openUrl(row.url)"
|
||||
>
|
||||
预览
|
||||
</NButton>
|
||||
<NButton v-else type="link" @click="() => openUrl(row.url)">
|
||||
下载
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraFileConfigApi } from '#/api/infra/file-config';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
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'],
|
||||
show: (formValues) => !formValues.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
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,
|
||||
controlsPosition: 'right',
|
||||
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.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: '请输入配置名',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'storage',
|
||||
label: '存储器',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE, 'number'),
|
||||
placeholder: '请选择存储器',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = InfraFileConfigApi.FileConfig>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '配置名',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'storage',
|
||||
title: '存储器',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_FILE_STORAGE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '主配置',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '文件配置',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['infra:file-config:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['infra:file-config:delete']),
|
||||
},
|
||||
{
|
||||
code: 'master',
|
||||
text: '主配置',
|
||||
disabled: (row: any) => row.master,
|
||||
show: (_row: any) => hasAccessByCodes(['infra:file-config:update']),
|
||||
},
|
||||
{
|
||||
code: 'test',
|
||||
text: '测试',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraFileConfigApi } from '#/api/infra/file-config';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteFileConfig,
|
||||
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 onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建文件配置 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑文件配置 */
|
||||
function onEdit(row: InfraFileConfigApi.FileConfig) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 设为主配置 */
|
||||
async function onMaster(row: InfraFileConfigApi.FileConfig) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.updating', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await updateFileConfigMaster(row.id as number);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试文件配置 */
|
||||
async function onTest(row: InfraFileConfigApi.FileConfig) {
|
||||
const hideLoading = message.loading({
|
||||
content: '测试上传中...',
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
const response = await testFileConfig(row.id as number);
|
||||
hideLoading();
|
||||
// 确认是否访问该文件
|
||||
confirm({
|
||||
title: '测试上传成功',
|
||||
content: '是否要访问该文件?',
|
||||
confirmText: '访问',
|
||||
cancelText: '取消',
|
||||
}).then(() => {
|
||||
openWindow(response);
|
||||
});
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除文件配置 */
|
||||
async function onDelete(row: InfraFileConfigApi.FileConfig) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteFileConfig(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraFileConfigApi.FileConfig>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'master': {
|
||||
onMaster(row);
|
||||
break;
|
||||
}
|
||||
case 'test': {
|
||||
onTest(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getFileConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraFileConfigApi.FileConfig>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="文件配置列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['infra:file-config:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['文件配置']) }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -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: 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 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 as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, InfraJobStatusEnum } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
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: '请输入处理器的名字',
|
||||
// readonly: ({ values }) => !!values.id,
|
||||
},
|
||||
rules: 'required',
|
||||
// TODO @芋艿:在修改场景下,禁止调整
|
||||
},
|
||||
{
|
||||
fieldName: 'handlerParam',
|
||||
label: '处理器的参数',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入处理器的参数',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'cronExpression',
|
||||
label: 'CRON 表达式',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 CRON 表达式',
|
||||
},
|
||||
rules: 'required',
|
||||
// TODO @芋艿:未来支持动态的 CRON 表达式选择
|
||||
},
|
||||
{
|
||||
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<T = InfraJobApi.Job>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 280,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '任务',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['infra:job:update']),
|
||||
},
|
||||
{
|
||||
code: 'update-status',
|
||||
text: '开启',
|
||||
show: (row: any) =>
|
||||
hasAccessByCodes(['infra:job:update']) &&
|
||||
row.status === InfraJobStatusEnum.STOP,
|
||||
},
|
||||
{
|
||||
code: 'update-status',
|
||||
text: '暂停',
|
||||
show: (row: any) =>
|
||||
hasAccessByCodes(['infra:job:update']) &&
|
||||
row.status === InfraJobStatusEnum.NORMAL,
|
||||
},
|
||||
{
|
||||
code: 'trigger',
|
||||
text: '执行',
|
||||
show: hasAccessByCodes(['infra:job:trigger']),
|
||||
},
|
||||
// TODO @芋艿:增加一个“更多”选项
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详细',
|
||||
show: hasAccessByCodes(['infra:job:query']),
|
||||
},
|
||||
{
|
||||
code: 'log',
|
||||
text: '日志',
|
||||
show: hasAccessByCodes(['infra:job:query']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['infra:job:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, History, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteJob,
|
||||
exportJob,
|
||||
getJobPage,
|
||||
runJob,
|
||||
updateJobStatus,
|
||||
} from '#/api/infra/job';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
import { InfraJobStatusEnum } from '#/utils';
|
||||
|
||||
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 onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportJob(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '定时任务.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建任务 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑任务 */
|
||||
function onEdit(row: InfraJobApi.Job) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 查看任务详情 */
|
||||
function onDetail(row: InfraJobApi.Job) {
|
||||
detailModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 更新任务状态 */
|
||||
async function onUpdateStatus(row: InfraJobApi.Job) {
|
||||
const status =
|
||||
row.status === InfraJobStatusEnum.STOP
|
||||
? InfraJobStatusEnum.NORMAL
|
||||
: InfraJobStatusEnum.STOP;
|
||||
const statusText = status === InfraJobStatusEnum.NORMAL ? '启用' : '停用';
|
||||
|
||||
confirm({
|
||||
content: `确定${statusText} ${row.name} 吗?`,
|
||||
}).then(async () => {
|
||||
await updateJobStatus(row.id as number, status);
|
||||
// 提示成功
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
/** 执行一次任务 */
|
||||
async function onTrigger(row: InfraJobApi.Job) {
|
||||
confirm({
|
||||
content: `确定执行一次 ${row.name} 吗?`,
|
||||
}).then(async () => {
|
||||
await runJob(row.id as number);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
});
|
||||
}
|
||||
|
||||
/** 跳转到任务日志 */
|
||||
function onLog(row?: InfraJobApi.Job) {
|
||||
push({
|
||||
name: 'InfraJobLog',
|
||||
query: row?.id ? { id: row.id } : {},
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除任务 */
|
||||
async function onDelete(row: InfraJobApi.Job) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteJob(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<InfraJobApi.Job>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'log': {
|
||||
onLog(row);
|
||||
break;
|
||||
}
|
||||
case 'trigger': {
|
||||
onTrigger(row);
|
||||
break;
|
||||
}
|
||||
case 'update-status': {
|
||||
onUpdateStatus(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getJobPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InfraJobApi.Job>,
|
||||
});
|
||||
</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="onRefresh" />
|
||||
<DetailModal />
|
||||
<Grid table-title="定时任务列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['infra:job:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['任务']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['infra:job:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onLog(undefined)"
|
||||
v-access:code="['infra:job:query']"
|
||||
>
|
||||
<History class="size-5" />
|
||||
执行日志
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
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<T = InfraJobLogApi.JobLog>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): 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 },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'id',
|
||||
nameTitle: '日志',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详细',
|
||||
show: hasAccessByCodes(['infra:job:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { exportJobLog, getJobLogPage } from '#/api/infra/job-log';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
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 onExport() {
|
||||
const data = await exportJobLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '任务日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看日志详情 */
|
||||
function onDetail(row: InfraJobLogApi.JobLog) {
|
||||
detailModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<InfraJobLogApi.JobLog>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表单schema并设置默认jobId
|
||||
const formSchema = useGridFormSchema();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: formSchema,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
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',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
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>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['infra:job:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
import { getJobLog } from '#/api/infra/job-log';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<InfraJobLogApi.JobLog>();
|
||||
|
||||
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"
|
||||
>
|
||||
<NDescriptions
|
||||
:column="1"
|
||||
bordered
|
||||
size="middle"
|
||||
class="mx-4"
|
||||
:label-style="{ width: '140px' }"
|
||||
>
|
||||
<NDescriptionsItem label="日志编号">
|
||||
{{ formData?.id }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="任务编号">
|
||||
{{ formData?.jobId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="处理器的名字">
|
||||
{{ formData?.handlerName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="处理器的参数">
|
||||
{{ formData?.handlerParam }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="第几次执行">
|
||||
{{ formData?.executeIndex }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="执行时间">
|
||||
{{ formData?.beginTime ? formatDateTime(formData.beginTime) : '' }} ~
|
||||
{{ formData?.endTime ? formatDateTime(formData.endTime) : '' }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="执行时长">
|
||||
{{ formData?.duration ? `${formData.duration} 毫秒` : '' }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="任务状态">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.INFRA_JOB_LOG_STATUS"
|
||||
:value="formData?.status"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="执行结果">
|
||||
{{ formData?.result }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
NDescriptions,
|
||||
NDescriptionsItem,
|
||||
NTimeline,
|
||||
NTimelineItem,
|
||||
} from 'naive-ui';
|
||||
|
||||
import { getJob, getJobNextTimes } from '#/api/infra/job';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<InfraJobApi.Job>(); // 任务详情
|
||||
const nextTimes = ref<Date[]>([]); // 下一次执行时间
|
||||
|
||||
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);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="任务详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<NDescriptions
|
||||
:column="1"
|
||||
bordered
|
||||
size="middle"
|
||||
class="mx-4"
|
||||
:label-style="{ width: '140px' }"
|
||||
>
|
||||
<NDescriptionsItem label="任务编号">
|
||||
{{ formData?.id }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="任务名称">
|
||||
{{ formData?.name }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="任务状态">
|
||||
<DictTag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="formData?.status" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="处理器的名字">
|
||||
{{ formData?.handlerName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="处理器的参数">
|
||||
{{ formData?.handlerParam }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="Cron 表达式">
|
||||
{{ formData?.cronExpression }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="重试次数">
|
||||
{{ formData?.retryCount }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="重试间隔">
|
||||
{{
|
||||
formData?.retryInterval ? `${formData.retryInterval} 毫秒` : '无间隔'
|
||||
}}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="监控超时时间">
|
||||
{{
|
||||
formData?.monitorTimeout && formData.monitorTimeout > 0
|
||||
? `${formData.monitorTimeout} 毫秒`
|
||||
: '未开启'
|
||||
}}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="后续执行时间">
|
||||
<NTimeline class="h-[180px]">
|
||||
<NTimelineItem
|
||||
v-for="(nextTime, index) in nextTimes"
|
||||
:key="index"
|
||||
color="blue"
|
||||
>
|
||||
第 {{ index + 1 }} 次:{{ formatDateTime(nextTime.toString()) }}
|
||||
</NTimelineItem>
|
||||
</NTimeline>
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -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-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { NCard } from 'naive-ui';
|
||||
|
||||
import { getRedisMonitorInfo } from '#/api/infra/redis';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import Commands from './modules/commands.vue';
|
||||
import Info from './modules/info.vue';
|
||||
import Memory from './modules/memory.vue';
|
||||
|
||||
const redisData = ref<InfraRedisApi.RedisMonitorInfo>();
|
||||
|
||||
/** 统一加载 Redis 数据 */
|
||||
const loadRedisData = async () => {
|
||||
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 class="mt-5" 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>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<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);
|
||||
|
||||
/** 渲染命令统计图表 */
|
||||
const 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>
|
||||
<div>
|
||||
<EchartsUI ref="chartRef" height="420px" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
defineProps<{
|
||||
redisData?: InfraRedisApi.RedisMonitorInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDescriptions
|
||||
:column="6"
|
||||
bordered
|
||||
size="middle"
|
||||
:label-style="{ width: '138px' }"
|
||||
>
|
||||
<NDescriptionsItem label="Redis 版本">
|
||||
{{ redisData?.info?.redis_version }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="运行模式">
|
||||
{{ redisData?.info?.redis_mode === 'standalone' ? '单机' : '集群' }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="端口">
|
||||
{{ redisData?.info?.tcp_port }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="客户端数">
|
||||
{{ redisData?.info?.connected_clients }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="运行时间(天)">
|
||||
{{ redisData?.info?.uptime_in_days }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="使用内存">
|
||||
{{ redisData?.info?.used_memory_human }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="使用 CPU">
|
||||
{{
|
||||
redisData?.info
|
||||
? parseFloat(redisData?.info?.used_cpu_user_children).toFixed(2)
|
||||
: ''
|
||||
}}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="内存配置">
|
||||
{{ redisData?.info?.maxmemory_human }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="AOF 是否开启">
|
||||
{{ redisData?.info?.aof_enabled === '0' ? '否' : '是' }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="RDB 是否成功">
|
||||
{{ redisData?.info?.rdb_last_bgsave_status }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="Key 数量">
|
||||
{{ redisData?.dbSize }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="网络入口/出口">
|
||||
{{ redisData?.info?.instantaneous_input_kbps }}kps /
|
||||
{{ redisData?.info?.instantaneous_output_kbps }}kps
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<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);
|
||||
|
||||
/** 解析内存值,移除单位,转为数字 */
|
||||
const 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;
|
||||
}
|
||||
};
|
||||
|
||||
/** 渲染内存使用图表 */
|
||||
const 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>
|
||||
<div>
|
||||
<EchartsUI ref="chartRef" height="420px" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { IFrame } from '#/components/iframe';
|
||||
|
||||
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>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { IFrame } from '#/components/iframe';
|
||||
|
||||
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>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { getConfigKey } from '#/api/infra/config';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { IFrame } from '#/components/iframe';
|
||||
|
||||
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>
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { computed, onMounted, ref, watchEffect } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { useWebSocket } from '@vueuse/core';
|
||||
import {
|
||||
NAvatar,
|
||||
NBadge,
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NEmpty,
|
||||
NInput,
|
||||
NSelect,
|
||||
NSelectOption,
|
||||
NTag,
|
||||
} from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapters/message';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
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 ? 'success' : '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(''); // 发送人
|
||||
const handlerSend = () => {
|
||||
if (!sendText.value.trim()) {
|
||||
message.warning('消息内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 先 JSON 化 message 消息内容
|
||||
const messageContent = JSON.stringify({
|
||||
text: sendText.value,
|
||||
toUserId: sendUserId.value,
|
||||
});
|
||||
// 1.2 再 JSON 化整个消息
|
||||
const jsonMessage = JSON.stringify({
|
||||
type: 'demo-message-send',
|
||||
content: messageContent,
|
||||
});
|
||||
// 2. 最后发送消息
|
||||
send(jsonMessage);
|
||||
sendText.value = '';
|
||||
};
|
||||
|
||||
/** 切换 websocket 连接状态 */
|
||||
const toggleConnectStatus = () => {
|
||||
if (getIsOpen.value) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取消息类型的徽标颜色 */
|
||||
const getMessageBadgeColor = (type?: string) => {
|
||||
switch (type) {
|
||||
case 'group': {
|
||||
return 'green';
|
||||
}
|
||||
case 'single': {
|
||||
return 'blue';
|
||||
}
|
||||
case 'system': {
|
||||
return 'red';
|
||||
}
|
||||
default: {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取消息类型的文本 */
|
||||
const getMessageTypeText = (type?: string) => {
|
||||
switch (type) {
|
||||
case 'group': {
|
||||
return '群发';
|
||||
}
|
||||
case 'single': {
|
||||
return '单发';
|
||||
}
|
||||
case 'system': {
|
||||
return '系统';
|
||||
}
|
||||
default: {
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
const userList = ref<SystemUserApi.User[]>([]); // 用户列表
|
||||
onMounted(async () => {
|
||||
userList.value = await getSimpleUserList();
|
||||
});
|
||||
</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 #title>
|
||||
<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 #addonBefore>
|
||||
<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"
|
||||
>
|
||||
<NSelectOption key="" value="" label="所有人">
|
||||
<div class="flex items-center">
|
||||
<NAvatar size="small" class="mr-2">全</NAvatar>
|
||||
<span>所有人</span>
|
||||
</div>
|
||||
</NSelectOption>
|
||||
<NSelectOption
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:value="user.id"
|
||||
:label="user.nickname"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<NAvatar size="small" class="mr-2">
|
||||
{{ user.nickname.slice(0, 1) }}
|
||||
</NAvatar>
|
||||
<span>{{ user.nickname }}</span>
|
||||
</div>
|
||||
</NSelectOption>
|
||||
</NSelect>
|
||||
|
||||
<NInput
|
||||
type="textarea"
|
||||
v-model:value="sendText"
|
||||
: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>
|
||||
<span class="i-ant-design:send-outlined mr-1"></span>
|
||||
</template>
|
||||
发送消息
|
||||
</NButton>
|
||||
</NCard>
|
||||
|
||||
<!-- 右侧:消息记录 -->
|
||||
<NCard :bordered="false" class="w-full md:w-1/2">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<span class="i-ant-design:message-outlined mr-2 text-lg"></span>
|
||||
<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>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemAreaApi } from '#/api/system/area';
|
||||
|
||||
/** 查询 IP 的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'ip',
|
||||
label: 'IP 地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 IP 地址',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'result',
|
||||
label: '地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '展示查询 IP 结果',
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<SystemAreaApi.Area>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '地区编码',
|
||||
minWidth: 120,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '地区名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Search } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 查询 IP */
|
||||
function onQueryIp() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return await getAreaTree();
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
},
|
||||
treeConfig: {
|
||||
rowField: 'id',
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="地区 & IP" url="https://doc.iocoder.cn/area-and-ip/" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="地区列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton type="primary" @click="onQueryIp">
|
||||
<Search class="size-5" />
|
||||
IP 查询
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getAreaByIp } from '#/api/system/area';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const [Form, { setFieldValue, validate, getValues }] = 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 validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await getValues();
|
||||
try {
|
||||
const result = await getAreaByIp(data.ip);
|
||||
// 设置结果
|
||||
await setFieldValue('result', result);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="IP 查询">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
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 getDeptList();
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级部门',
|
||||
});
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级部门',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '部门名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入部门名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入显示顺序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'leaderUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择负责人',
|
||||
allowClear: true,
|
||||
},
|
||||
rules: z.number().optional(),
|
||||
},
|
||||
{
|
||||
fieldName: 'phone',
|
||||
label: '联系电话',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 11,
|
||||
placeholder: '请输入联系电话',
|
||||
},
|
||||
rules: 'mobileRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
rules: z.string().email('请输入正确的邮箱地址').optional(),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
onActionClick?: OnActionClickFn<SystemDeptApi.Dept>,
|
||||
getLeaderName?: (userId: number) => string | undefined,
|
||||
): VxeTableGridOptions<SystemDeptApi.Dept>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '部门名称',
|
||||
minWidth: 150,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'leaderUserId',
|
||||
title: '负责人',
|
||||
minWidth: 150,
|
||||
formatter: (row) => {
|
||||
return getLeaderName?.(row.cellValue) || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示顺序',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '部门状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 200,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '部门',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'append',
|
||||
text: '新增下级',
|
||||
show: hasAccessByCodes(['system:dept:create']),
|
||||
},
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:dept:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:dept:delete']),
|
||||
disabled: (row: SystemDeptApi.Dept) => {
|
||||
return !!(row.children && row.children.length > 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteDept, getDeptList } from '#/api/system/dept';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const userList = ref<SystemUserApi.User[]>([]);
|
||||
|
||||
/** 获取负责人名称 */
|
||||
const getLeaderName = (userId: number) => {
|
||||
return userList.value.find((user) => user.id === userId)?.nickname;
|
||||
};
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
/** 创建部门 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 添加下级部门 */
|
||||
function onAppend(row: SystemDeptApi.Dept) {
|
||||
formModalApi.setData({ parentId: row.id }).open();
|
||||
}
|
||||
|
||||
/** 编辑部门 */
|
||||
function onEdit(row: SystemDeptApi.Dept) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
async function onDelete(row: SystemDeptApi.Dept) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteDept(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<SystemDeptApi.Dept>) {
|
||||
switch (code) {
|
||||
case 'append': {
|
||||
onAppend(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick, getLeaderName),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
return await getDeptList();
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
expandAll: true,
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
userList.value = await getSimpleUserList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="部门列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:dept:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['部门']) }}
|
||||
</NButton>
|
||||
<NButton class="ml-2" @click="toggleExpand">
|
||||
{{ isExpanded ? '收缩' : '展开' }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createDept, getDept, updateDept } from '#/api/system/dept';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemDeptApi.Dept>();
|
||||
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 SystemDeptApi.Dept;
|
||||
try {
|
||||
await (formData.value?.id ? updateDept(data) : createDept(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<SystemDeptApi.Dept>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getDept(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,388 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemDictDataApi } from '#/api/system/dict/data';
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict/type';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleDictTypeList } from '#/api/system/dict/type';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
// ============================== 字典类型 ==============================
|
||||
|
||||
/** 类型新增/修改的表单 */
|
||||
export function useTypeFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '字典名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入字典名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '字典类型',
|
||||
component: 'Input',
|
||||
componentProps: (values) => {
|
||||
return {
|
||||
placeholder: '请输入字典类型',
|
||||
disabled: !!values.id,
|
||||
};
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 类型列表的搜索表单 */
|
||||
export function useTypeGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '字典名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入字典名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 类型列表的字段 */
|
||||
export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '字典编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '字典名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '字典类型',
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
minWidth: 120,
|
||||
title: '操作',
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'type',
|
||||
nameTitle: '字典类型',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:dict:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:dict:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============================== 字典数据 ==============================
|
||||
|
||||
// TODO @芋艿:后续针对 antd,增加
|
||||
/**
|
||||
* 颜色选项
|
||||
*/
|
||||
const colorOptions = [
|
||||
{ value: '', label: '无' },
|
||||
{ value: 'processing', label: '主要' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'default', label: '默认' },
|
||||
{ value: 'warning', label: '警告' },
|
||||
{ value: 'error', label: '危险' },
|
||||
{ value: 'pink', label: 'pink' },
|
||||
{ value: 'red', label: 'red' },
|
||||
{ value: 'orange', label: 'orange' },
|
||||
{ value: 'green', label: 'green' },
|
||||
{ value: 'cyan', label: 'cyan' },
|
||||
{ value: 'blue', label: 'blue' },
|
||||
{ value: 'purple', label: 'purple' },
|
||||
];
|
||||
|
||||
/** 数据新增/修改的表单 */
|
||||
export function useDataFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'dictType',
|
||||
label: '字典类型',
|
||||
component: 'ApiSelect',
|
||||
componentProps: (values) => {
|
||||
return {
|
||||
api: getSimpleDictTypeList,
|
||||
placeholder: '请输入字典类型',
|
||||
labelField: 'name',
|
||||
valueField: 'type',
|
||||
disabled: !!values.id,
|
||||
};
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'label',
|
||||
label: '数据标签',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入数据标签',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
label: '数据键值',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入数据键值',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入显示排序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
placeholder: '请选择状态',
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'colorType',
|
||||
label: '颜色类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: colorOptions,
|
||||
placeholder: '请选择颜色类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'cssClass',
|
||||
label: 'CSS Class',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 CSS Class',
|
||||
},
|
||||
help: '输入 hex 模式的颜色, 例如 #108ee9',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 字典数据列表搜索表单 */
|
||||
export function useDataGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'label',
|
||||
label: '字典标签',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入字典标签',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据表格列
|
||||
*/
|
||||
export function useDataGridColumns<T = SystemDictDataApi.DictData>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '字典编码',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'label',
|
||||
title: '字典标签',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '字典键值',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '字典排序',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'colorType',
|
||||
title: '颜色类型',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'cssClass',
|
||||
title: 'CSS Class',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
minWidth: 120,
|
||||
title: '操作',
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'label',
|
||||
nameTitle: '字典数据',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:dict:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:dict:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import DataGrid from './modules/data-grid.vue';
|
||||
import TypeGrid from './modules/type-grid.vue';
|
||||
|
||||
const searchDictType = ref<string>(); // 搜索的字典类型
|
||||
|
||||
function onDictTypeSelect(dictType: string) {
|
||||
searchDictType.value = dictType;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="字典管理" url="https://doc.iocoder.cn/system-dict/" />
|
||||
</template>
|
||||
|
||||
<div class="flex h-full">
|
||||
<!-- 左侧字典类型列表 -->
|
||||
<div class="w-1/2 pr-3">
|
||||
<TypeGrid @select="onDictTypeSelect" />
|
||||
</div>
|
||||
<!-- 右侧字典数据列表 -->
|
||||
<div class="w-1/2">
|
||||
<DataGrid :dict-type="searchDictType" />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemDictDataApi } from '#/api/system/dict/data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDictData,
|
||||
getDictData,
|
||||
updateDictData,
|
||||
} from '#/api/system/dict/data';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDataFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'SystemDictDataForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemDictDataApi.DictData>();
|
||||
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: useDataFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as SystemDictDataApi.DictData;
|
||||
try {
|
||||
await (formData.value?.id ? updateDictData(data) : createDictData(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<
|
||||
SystemDictDataApi.DictData | { dictType?: string }
|
||||
>();
|
||||
|
||||
// 如果有ID,表示是编辑
|
||||
if (data && 'id' in data && data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDictData(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
} else if (data && 'dictType' in data && data.dictType) {
|
||||
// 新增时,如果传入了dictType,则需要设置
|
||||
await formApi.setValues({
|
||||
dictType: data.dictType,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemDictDataApi } from '#/api/system/dict/data';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDictData,
|
||||
exportDictData,
|
||||
getDictDataPage,
|
||||
} from '#/api/system/dict/data';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDataGridColumns, useDataGridFormSchema } from '../data';
|
||||
import DataForm from './data-form.vue';
|
||||
|
||||
const props = defineProps({
|
||||
dictType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const [DataFormModal, dataFormModalApi] = useVbenModal({
|
||||
connectedComponent: DataForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportDictData(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '字典数据.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建字典数据 */
|
||||
function onCreate() {
|
||||
dataFormModalApi.setData({ dictType: props.dictType }).open();
|
||||
}
|
||||
|
||||
/** 编辑字典数据 */
|
||||
function onEdit(row: any) {
|
||||
dataFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除字典数据 */
|
||||
async function onDelete(row: any) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('common.processing'),
|
||||
duration: 0,
|
||||
key: 'process_message',
|
||||
});
|
||||
try {
|
||||
await deleteDictData(row.id);
|
||||
message.success({
|
||||
content: $t('common.operationSuccess'),
|
||||
key: 'process_message',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮回调 */
|
||||
function onActionClick({ code, row }: OnActionClickParams) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useDataGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useDataGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDictDataPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
dictType: props.dictType,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemDictDataApi.DictData>,
|
||||
});
|
||||
|
||||
/** 监听 dictType 变化,重新查询 */
|
||||
watch(
|
||||
() => props.dictType,
|
||||
() => {
|
||||
if (props.dictType) {
|
||||
onRefresh();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<DataFormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="字典数据列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:dict:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['字典数据']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:dict:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict/type';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createDictType,
|
||||
getDictType,
|
||||
updateDictType,
|
||||
} from '#/api/system/dict/type';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useTypeFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemDictTypeApi.DictType>();
|
||||
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: useTypeFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as SystemDictTypeApi.DictType;
|
||||
try {
|
||||
await (formData.value?.id ? updateDictType(data) : createDictType(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<SystemDictTypeApi.DictType>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDictType(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeGridListeners,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict/type';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDictType,
|
||||
exportDictType,
|
||||
getDictTypePage,
|
||||
} from '#/api/system/dict/type';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useTypeGridColumns, useTypeGridFormSchema } from '../data';
|
||||
import TypeForm from './type-form.vue';
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
|
||||
const [TypeFormModal, typeFormModalApi] = useVbenModal({
|
||||
connectedComponent: TypeForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportDictType(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '字典类型.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建字典类型 */
|
||||
function onCreate() {
|
||||
typeFormModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑字典类型 */
|
||||
function onEdit(row: any) {
|
||||
typeFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除字典类型 */
|
||||
async function onDelete(row: SystemDictTypeApi.DictType) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('common.processing'),
|
||||
duration: 0,
|
||||
key: 'process_message',
|
||||
});
|
||||
try {
|
||||
await deleteDictType(row.id as number);
|
||||
message.success({
|
||||
content: $t('common.operationSuccess'),
|
||||
key: 'process_message',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮回调 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemDictTypeApi.DictType>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格事件 */
|
||||
const gridEvents: VxeGridListeners<SystemDictTypeApi.DictType> = {
|
||||
cellClick: ({ row }) => {
|
||||
emit('select', row.type);
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useTypeGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useTypeGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDictTypePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemDictTypeApi.DictType>,
|
||||
gridEvents,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<TypeFormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="字典类型列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:dict:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['字典类型']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:dict:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'username',
|
||||
label: '用户名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入用户名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userIp',
|
||||
label: '登录地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入登录地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '登录时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemLoginLogApi.LoginLog>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'logType',
|
||||
title: '操作类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_LOGIN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
title: '用户名称',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'userIp',
|
||||
title: '登录地址',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'userAgent',
|
||||
title: '浏览器',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'result',
|
||||
title: '登录结果',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_LOGIN_RESULT },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '登录日期',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'username',
|
||||
nameTitle: '登录日志',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['system:login-log:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { exportLoginLog, getLoginLogPage } from '#/api/system/login-log';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportLoginLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '登录日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看登录日志详情 */
|
||||
function onDetail(row: SystemLoginLogApi.LoginLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemLoginLogApi.LoginLog>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getLoginLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemLoginLogApi.LoginLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="onRefresh" />
|
||||
<Grid table-title="登录日志列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:login-log:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<SystemLoginLogApi.LoginLog>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemLoginLogApi.LoginLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="登录日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<NDescriptions
|
||||
bordered
|
||||
:column="1"
|
||||
size="middle"
|
||||
class="mx-4"
|
||||
:label-style="{ width: '110px' }"
|
||||
>
|
||||
<NDescriptionsItem label="日志编号">
|
||||
{{ formData?.id }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.SYSTEM_LOGIN_TYPE"
|
||||
:value="formData?.logType"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户名称">
|
||||
{{ formData?.username }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="登录地址">
|
||||
{{ formData?.userIp }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="浏览器">
|
||||
{{ formData?.userAgent }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="登录结果">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.SYSTEM_LOGIN_RESULT"
|
||||
:value="formData?.result"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="登录日期">
|
||||
{{ formatDateTime(formData?.createTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMailAccountApi } from '#/api/system/mail/account';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mail',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'host',
|
||||
label: 'SMTP 服务器域名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 SMTP 服务器域名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'port',
|
||||
label: 'SMTP 服务器端口',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入 SMTP 服务器端口',
|
||||
min: 0,
|
||||
max: 65_535,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sslEnable',
|
||||
label: '是否开启 SSL',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.boolean().default(true),
|
||||
},
|
||||
{
|
||||
fieldName: 'starttlsEnable',
|
||||
label: '是否开启 STARTTLS',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.boolean().default(false),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'mail',
|
||||
label: '邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemMailAccountApi.MailAccount>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'mail',
|
||||
title: '邮箱',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
title: '用户名',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
title: 'SMTP 服务器域名',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'port',
|
||||
title: 'SMTP 服务器端口',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'sslEnable',
|
||||
title: '是否开启 SSL',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'starttlsEnable',
|
||||
title: '是否开启 STARTTLS',
|
||||
minWidth: 145,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'mail',
|
||||
nameTitle: '邮箱账号',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:mail-account:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:mail-account:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemMailAccountApi } from '#/api/system/mail/account';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteMailAccount,
|
||||
getMailAccountPage,
|
||||
} from '#/api/system/mail/account';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建邮箱账号 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑邮箱账号 */
|
||||
function onEdit(row: SystemMailAccountApi.MailAccount) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除邮箱账号 */
|
||||
async function onDelete(row: SystemMailAccountApi.MailAccount) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.mail]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteMailAccount(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.mail]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemMailAccountApi.MailAccount>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMailAccountPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemMailAccountApi.MailAccount>,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="邮件配置" url="https://doc.iocoder.cn/mail" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="邮箱账号列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:mail-account:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['邮箱账号']) }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemMailAccountApi } from '#/api/system/mail/account';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createMailAccount,
|
||||
getMailAccount,
|
||||
updateMailAccount,
|
||||
} from '#/api/system/mail/account';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemMailAccountApi.MailAccount>();
|
||||
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 SystemMailAccountApi.MailAccount;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateMailAccount(data)
|
||||
: createMailAccount(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<SystemMailAccountApi.MailAccount>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getMailAccount(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { getSimpleMailAccountList } from '#/api/system/mail/account';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'sendTime',
|
||||
label: '发送时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
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: 'sendStatus',
|
||||
label: '发送状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择发送状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'accountId',
|
||||
label: '邮箱账号',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => await getSimpleMailAccountList(),
|
||||
labelField: 'mail',
|
||||
valueField: 'id',
|
||||
allowClear: true,
|
||||
placeholder: '请选择邮箱账号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'templateId',
|
||||
label: '模板编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入模板编号',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemMailLogApi.MailLog>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'sendTime',
|
||||
title: '发送时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'toMail',
|
||||
title: '收件邮箱',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'templateTitle',
|
||||
title: '邮件标题',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'templateContent',
|
||||
title: '邮件内容',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'fromMail',
|
||||
title: '发送邮箱',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'sendStatus',
|
||||
title: '发送状态',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'templateCode',
|
||||
title: '模板编码',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'toMail',
|
||||
nameTitle: '邮件日志',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '查看',
|
||||
show: hasAccessByCodes(['system:mail-log:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getMailLogPage } from '#/api/system/mail/log';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 查看邮件日志 */
|
||||
function onDetail(row: SystemMailLogApi.MailLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemMailLogApi.MailLog>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMailLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemMailLogApi.MailLog>,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="邮件配置" url="https://doc.iocoder.cn/mail" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="onRefresh" />
|
||||
<Grid table-title="邮件日志列表">
|
||||
<template #toolbar-tools> </template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<SystemMailLogApi.MailLog>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemMailLogApi.MailLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="邮件日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<div class="p-4">
|
||||
<NDescriptions :column="2" bordered :label-style="{ width: '140px' }">
|
||||
<NDescriptionsItem label="编号">{{ formData?.id }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="创建时间">
|
||||
{{ formatDateTime(formData?.createTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="收件邮箱">
|
||||
{{ formData?.toMail }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发送邮箱">
|
||||
{{ formData?.fromMail }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户编号">
|
||||
{{ formData?.userId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户类型">
|
||||
{{ formData?.userType }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模板编号">
|
||||
{{ formData?.templateId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模板编码">
|
||||
{{ formData?.templateCode }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="邮件标题" :span="2">
|
||||
{{ formData?.templateTitle }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="邮件内容" :span="2">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="formData?.templateContent"></div>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发送状态">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.SYSTEM_MAIL_SEND_STATUS"
|
||||
:value="formData?.sendStatus"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发送时间">
|
||||
{{ formatDateTime(formData?.sendTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发送消息编号">
|
||||
{{ formData?.sendMessageId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发送异常">
|
||||
{{ formData?.sendException }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleMailAccountList } from '#/api/system/mail/account';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '模板编码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板编码',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'accountId',
|
||||
label: '邮箱账号',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => await getSimpleMailAccountList(),
|
||||
labelField: 'mail',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择邮箱账号',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '发送人名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入发送人名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '模板标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板标题',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '模板内容',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板内容',
|
||||
height: 300,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '开启状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 发送邮件表单 */
|
||||
export function useSendMailFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'templateParams',
|
||||
label: '模板参数',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '模板内容',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mail',
|
||||
label: '收件邮箱',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入收件邮箱',
|
||||
},
|
||||
rules: z.string().email('请输入正确的邮箱'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '开启状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择开启状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '模板编码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入模板编码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入模板名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'accountId',
|
||||
label: '邮箱账号',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => await getSimpleMailAccountList(),
|
||||
labelField: 'mail',
|
||||
valueField: 'id',
|
||||
allowClear: true,
|
||||
placeholder: '请选择邮箱账号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemMailTemplateApi.MailTemplate>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
getAccountMail?: (accountId: number) => string | undefined,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '模板编码',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '模板名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
title: '模板标题',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'accountId',
|
||||
title: '邮箱账号',
|
||||
minWidth: 120,
|
||||
formatter: (row) => getAccountMail?.(row.cellValue) || '-',
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '发送人名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '开启状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 150,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '邮件模板',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:mail-template:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:mail-template:delete']),
|
||||
},
|
||||
{
|
||||
code: 'send',
|
||||
text: '测试',
|
||||
show: hasAccessByCodes(['system:mail-template:send-mail']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemMailAccountApi } from '#/api/system/mail/account';
|
||||
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getSimpleMailAccountList } from '#/api/system/mail/account';
|
||||
import {
|
||||
deleteMailTemplate,
|
||||
getMailTemplatePage,
|
||||
} from '#/api/system/mail/template';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
import SendForm from './modules/send-form.vue';
|
||||
|
||||
const accountList = ref<SystemMailAccountApi.MailAccount[]>([]);
|
||||
|
||||
/** 获取邮箱账号 */
|
||||
const getAccountMail = (accountId: number) => {
|
||||
return accountList.value.find((account) => account.id === accountId)?.mail;
|
||||
};
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [SendModal, sendModalApi] = useVbenModal({
|
||||
connectedComponent: SendForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建邮件模板 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑邮件模板 */
|
||||
function onEdit(row: SystemMailTemplateApi.MailTemplate) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 发送测试邮件 */
|
||||
function onSend(row: SystemMailTemplateApi.MailTemplate) {
|
||||
sendModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除邮件模板 */
|
||||
async function onDelete(row: SystemMailTemplateApi.MailTemplate) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteMailTemplate(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemMailTemplateApi.MailTemplate>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'send': {
|
||||
onSend(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick, getAccountMail),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMailTemplatePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemMailTemplateApi.MailTemplate>,
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
accountList.value = await getSimpleMailAccountList();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="邮件配置" url="https://doc.iocoder.cn/mail" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<SendModal />
|
||||
<Grid table-title="邮件模板列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:mail-template:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['邮件模板']) }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createMailTemplate,
|
||||
getMailTemplate,
|
||||
updateMailTemplate,
|
||||
} from '#/api/system/mail/template';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemMailTemplateApi.MailTemplate>();
|
||||
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 SystemMailTemplateApi.MailTemplate;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateMailTemplate(data)
|
||||
: createMailTemplate(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<SystemMailTemplateApi.MailTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getMailTemplate(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemMailTemplateApi } from '#/api/system/mail/template';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { sendMail } from '#/api/system/mail/template';
|
||||
|
||||
import { useSendMailFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemMailTemplateApi.MailTemplate>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 构建发送请求
|
||||
const values = await formApi.getValues();
|
||||
const paramsObj: Record<string, string> = {};
|
||||
if (formData.value?.params) {
|
||||
formData.value.params.forEach((param: string) => {
|
||||
paramsObj[param] = values[`param_${param}`];
|
||||
});
|
||||
}
|
||||
const data: SystemMailTemplateApi.MailSendReqVO = {
|
||||
mail: values.mail,
|
||||
templateCode: formData.value?.code || '',
|
||||
templateParams: paramsObj,
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
try {
|
||||
await sendMail(data);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('邮件发送成功');
|
||||
} catch (error) {
|
||||
console.error('发送邮件失败', error);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 获取数据
|
||||
const data = modalApi.getData<SystemMailTemplateApi.MailTemplate>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formData.value = data;
|
||||
// 更新 form schema
|
||||
const schema = buildFormSchema();
|
||||
formApi.setState({ schema });
|
||||
// 设置到 values
|
||||
await formApi.setValues({
|
||||
content: data.content,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/** 动态构建表单 schema */
|
||||
const buildFormSchema = () => {
|
||||
const schema = useSendMailFormSchema();
|
||||
if (formData.value?.params) {
|
||||
formData.value.params?.forEach((param: string) => {
|
||||
schema.push({
|
||||
fieldName: `param_${param}`,
|
||||
label: `参数 ${param}`,
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: `请输入参数 ${param}`,
|
||||
},
|
||||
rules: 'required',
|
||||
});
|
||||
});
|
||||
}
|
||||
return schema;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="测试发送邮件">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { handleTree, isHttpUrl } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getMenuList } from '#/api/system/menu';
|
||||
import { $t } from '#/locales';
|
||||
import { componentKeys } from '#/router/routes';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
SystemMenuTypeEnum,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '上级菜单',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: async () => {
|
||||
const data = await getMenuList();
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级部门',
|
||||
} as SystemMenuApi.Menu);
|
||||
return handleTree(data);
|
||||
},
|
||||
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);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '菜单名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入菜单名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '菜单类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(SystemMenuTypeEnum.DIR),
|
||||
},
|
||||
{
|
||||
fieldName: 'icon',
|
||||
label: '菜单图标',
|
||||
component: 'IconPicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择菜单图标',
|
||||
prefix: 'carbon',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
|
||||
values.type,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'path',
|
||||
label: '路由地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入路由地址',
|
||||
},
|
||||
rules: z.string(),
|
||||
help: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头',
|
||||
dependencies: {
|
||||
triggerFields: ['type', 'parentId'],
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes(
|
||||
values.type,
|
||||
);
|
||||
},
|
||||
rules: (values) => {
|
||||
const schema = z.string().min(1, '路由地址不能为空');
|
||||
if (isHttpUrl(values.path)) {
|
||||
return schema;
|
||||
}
|
||||
if (values.parentId === 0) {
|
||||
return schema.refine(
|
||||
(path) => path.charAt(0) === '/',
|
||||
'路径必须以 / 开头',
|
||||
);
|
||||
}
|
||||
return schema.refine(
|
||||
(path) => path.charAt(0) !== '/',
|
||||
'路径不能以 / 开头',
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'component',
|
||||
label: '组件地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入组件地址',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'componentName',
|
||||
label: '组件名称',
|
||||
component: 'AutoComplete',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption(input: string, option: { value: string }) {
|
||||
return option.value.toLowerCase().includes(input.toLowerCase());
|
||||
},
|
||||
placeholder: '请选择组件名称',
|
||||
options: componentKeys.map((v) => ({ value: v })),
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'permission',
|
||||
label: '权限标识',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入菜单描述',
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.BUTTON, SystemMenuTypeEnum.MENU].includes(
|
||||
values.type,
|
||||
);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入显示顺序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '菜单状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'alwaysShow',
|
||||
label: '总是显示',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '总是', value: true },
|
||||
{ label: '不是', value: false },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
defaultValue: true,
|
||||
help: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'keepAlive',
|
||||
label: '缓存状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '缓存', value: true },
|
||||
{ label: '不缓存', value: false },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
defaultValue: true,
|
||||
help: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => {
|
||||
return [SystemMenuTypeEnum.MENU].includes(values.type);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(
|
||||
onActionClick: OnActionClickFn<SystemMenuApi.Menu>,
|
||||
): VxeTableGridOptions<SystemMenuApi.Menu>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '菜单名称',
|
||||
minWidth: 250,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '菜单类型',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_MENU_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示排序',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'permission',
|
||||
title: '权限标识',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'path',
|
||||
title: '组件路径',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'componentName',
|
||||
minWidth: 200,
|
||||
title: '组件名称',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
minWidth: 200,
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'append',
|
||||
text: '新增下级',
|
||||
show: hasAccessByCodes(['system:menu:create']),
|
||||
},
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:menu:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:menu:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon, Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteMenu, getMenuList } from '#/api/system/menu';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
import { SystemMenuTypeEnum } from '#/utils';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建菜单 */
|
||||
function onCreate() {
|
||||
formModalApi.setData({}).open();
|
||||
}
|
||||
|
||||
/** 添加下级菜单 */
|
||||
function onAppend(row: SystemMenuApi.Menu) {
|
||||
formModalApi.setData({ pid: row.id }).open();
|
||||
}
|
||||
|
||||
/** 编辑菜单 */
|
||||
function onEdit(row: SystemMenuApi.Menu) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除菜单 */
|
||||
async function onDelete(row: SystemMenuApi.Menu) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteMenu(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<SystemMenuApi.Menu>) {
|
||||
switch (code) {
|
||||
case 'append': {
|
||||
onAppend(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(false);
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
return await getMenuList();
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="功能权限"
|
||||
url="https://doc.iocoder.cn/resource-permission"
|
||||
/>
|
||||
<DocAlert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:menu:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['菜单']) }}
|
||||
</NButton>
|
||||
<NButton class="ml-2" @click="toggleExpand">
|
||||
{{ isExpanded ? '收缩' : '展开' }}
|
||||
</NButton>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<div class="size-5 flex-shrink-0">
|
||||
<IconifyIcon
|
||||
v-if="row.type === SystemMenuTypeEnum.BUTTON"
|
||||
icon="carbon:square-outline"
|
||||
class="size-full"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else-if="row.icon"
|
||||
:icon="row.icon || 'carbon:circle-dash'"
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-auto">{{ $t(row.name) }}</span>
|
||||
<div class="items-center justify-end"></div>
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createMenu, getMenu, updateMenu } from '#/api/system/menu';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemMenuApi.Menu>();
|
||||
const getTitle = computed(() =>
|
||||
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 SystemMenuApi.Menu;
|
||||
try {
|
||||
await (formData.value?.id ? updateMenu(data) : createMenu(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<SystemMenuApi.Menu>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getMenu(data.id as number);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(formData.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[40%]" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNoticeApi } from '#/api/system/notice';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '公告标题',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '公告类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '公告内容',
|
||||
component: 'RichTextarea',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '公告状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '公告标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入公告标题',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '公告状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
placeholder: '请选择公告状态',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemNoticeApi.Notice>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '公告编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
title: '公告标题',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '公告类型',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_NOTICE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '公告状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'title',
|
||||
nameTitle: '公告',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:notice:update']),
|
||||
},
|
||||
{
|
||||
code: 'push',
|
||||
text: '推送',
|
||||
show: hasAccessByCodes(['system:notice:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:notice:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemNoticeApi } from '#/api/system/notice';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteNotice, getNoticePage, pushNotice } from '#/api/system/notice';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建公告 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑公告 */
|
||||
function onEdit(row: SystemNoticeApi.Notice) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除公告 */
|
||||
async function onDelete(row: SystemNoticeApi.Notice) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.title]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteNotice(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.title]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 推送公告 */
|
||||
async function onPush(row: SystemNoticeApi.Notice) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.processing', ['推送']),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await pushNotice(row.id as number);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemNoticeApi.Notice>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'push': {
|
||||
onPush(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getNoticePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemNoticeApi.Notice>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="公告列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:notice:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['公告']) }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemNoticeApi } from '#/api/system/notice';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createNotice, getNotice, updateNotice } from '#/api/system/notice';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemNoticeApi.Notice>();
|
||||
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 SystemNoticeApi.Notice;
|
||||
try {
|
||||
await (formData.value?.id ? updateNotice(data) : createNotice(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<SystemNoticeApi.Notice>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getNotice(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/2">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
placeholder: '请选择用户类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'templateCode',
|
||||
label: '模板编码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入模板编码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'templateType',
|
||||
label: '模版类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
'number',
|
||||
),
|
||||
allowClear: true,
|
||||
placeholder: '请选择模版类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemNotifyMessageApi.NotifyMessage>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
title: '用户类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.USER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'templateCode',
|
||||
title: '模板编码',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'templateNickname',
|
||||
title: '发送人名称',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'templateContent',
|
||||
title: '模版内容',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'templateParams',
|
||||
title: '模版参数',
|
||||
minWidth: 180,
|
||||
formatter: ({ cellValue }) => {
|
||||
try {
|
||||
return JSON.stringify(cellValue);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'templateType',
|
||||
title: '模版类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
title: '是否已读',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
title: '阅读时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'id',
|
||||
nameTitle: '站内信',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['system:notify-message:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getNotifyMessagePage } from '#/api/system/notify/message';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 查看站内信详情 */
|
||||
function onDetail(row: SystemNotifyMessageApi.NotifyMessage) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemNotifyMessageApi.NotifyMessage>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getNotifyMessagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemNotifyMessageApi.NotifyMessage>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="短信配置" url="https://doc.iocoder.cn/sms/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="onRefresh" />
|
||||
<Grid table-title="站内信列表" />
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemNotifyMessageApi.NotifyMessage>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="站内信详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<NDescriptions bordered :column="1" size="middle" class="mx-4">
|
||||
<NDescriptionsItem label="编号">{{ formData?.id }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户类型">
|
||||
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="用户编号">
|
||||
{{ formData?.userId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模版编号">
|
||||
{{ formData?.templateId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模板编码">
|
||||
{{ formData?.templateCode }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发送人名称">
|
||||
{{ formData?.templateNickname }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模版内容">
|
||||
{{ formData?.templateContent }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模版参数">
|
||||
{{ formData?.templateParams }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="模版类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
|
||||
:value="formData?.templateType"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="是否已读">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
|
||||
:value="formData?.readStatus"
|
||||
/>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="阅读时间">
|
||||
{{ formatDateTime(formData?.readTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="创建时间">
|
||||
{{ formatDateTime(formData?.createTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'readStatus',
|
||||
label: '是否已读',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择是否已读',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '发送时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
...getRangePickerDefaultProps(),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemNotifyMessageApi.NotifyMessage>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
width: 40,
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
field: 'templateNickname',
|
||||
title: '发送人',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发送时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'templateType',
|
||||
title: '类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'templateContent',
|
||||
title: '消息内容',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
title: '是否已读',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
title: '阅读时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'id',
|
||||
nameTitle: '站内信',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '查看',
|
||||
show: (row: any) => row.readStatus,
|
||||
},
|
||||
{
|
||||
code: 'read',
|
||||
text: '已读',
|
||||
show: (row: any) => !row.readStatus,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'templateNickname',
|
||||
label: '发送人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '发送时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '消息类型',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
label: '是否已读',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.readStatus,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
label: '阅读时间',
|
||||
content: (data) => formatDateTime(data?.readTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'templateContent',
|
||||
label: '消息内容',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { MdiCheckboxMarkedCircleOutline } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getMyNotifyMessagePage,
|
||||
updateAllNotifyMessageRead,
|
||||
updateNotifyMessageRead,
|
||||
} from '#/api/system/notify/message';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 查看站内信详情 */
|
||||
function onDetail(row: SystemNotifyMessageApi.NotifyMessage) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 标记一条站内信已读 */
|
||||
async function onRead(row: SystemNotifyMessageApi.NotifyMessage) {
|
||||
message.loading({
|
||||
content: '正在标记已读...',
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
// 执行标记已读操作
|
||||
await updateNotifyMessageRead([row.id]);
|
||||
// 提示成功
|
||||
message.success('标记已读成功');
|
||||
onRefresh();
|
||||
|
||||
// 打开详情
|
||||
onDetail(row);
|
||||
}
|
||||
|
||||
/** 标记选中的站内信为已读 */
|
||||
async function onMarkRead() {
|
||||
const rows = gridApi.grid.getCheckboxRecords();
|
||||
if (!rows || rows.length === 0) {
|
||||
message.warning('请选择需要标记的站内信');
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = rows.map((row: SystemNotifyMessageApi.NotifyMessage) => row.id);
|
||||
message.loading({
|
||||
content: '正在标记已读...',
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
// 执行标记已读操作
|
||||
await updateNotifyMessageRead(ids);
|
||||
// 提示成功
|
||||
message.success('标记已读成功');
|
||||
await gridApi.grid.setAllCheckboxRow(false);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
/** 标记所有站内信为已读 */
|
||||
async function onMarkAllRead() {
|
||||
message.loading({
|
||||
content: '正在标记全部已读...',
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
// 执行标记已读操作
|
||||
await updateAllNotifyMessageRead();
|
||||
// 提示成功
|
||||
message.success('全部标记已读成功');
|
||||
await gridApi.grid.setAllCheckboxRow(false);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemNotifyMessageApi.NotifyMessage>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
case 'read': {
|
||||
onRead(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMyNotifyMessagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
checkboxConfig: {
|
||||
checkMethod: (params: { row: SystemNotifyMessageApi.NotifyMessage }) =>
|
||||
!params.row.readStatus,
|
||||
highlight: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemNotifyMessageApi.NotifyMessage>,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="站内信配置" url="https://doc.iocoder.cn/notify/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="onRefresh" />
|
||||
<Grid table-title="我的站内信">
|
||||
<template #toolbar-tools>
|
||||
<NButton type="primary" @click="onMarkRead">
|
||||
<MdiCheckboxMarkedCircleOutline />
|
||||
标记已读
|
||||
</NButton>
|
||||
<NButton type="primary" class="ml-2" @click="onMarkAllRead">
|
||||
<MdiCheckboxMarkedCircleOutline />
|
||||
全部已读
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const [Description, descApi] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
size: 'middle',
|
||||
class: 'mx-4',
|
||||
},
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemNotifyMessageApi.NotifyMessage>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
descApi.setState({ data });
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="消息详情"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Description />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
UserTypeEnum,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '模板编码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板编码',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '发送人名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入发送人名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '模板内容',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板内容',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '模板类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
'number',
|
||||
),
|
||||
placeholder: '请选择模板类型',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入模板名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '模板编码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入模板编码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '模板类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
'number',
|
||||
),
|
||||
allowClear: true,
|
||||
placeholder: '请选择模板类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 发送站内信表单 */
|
||||
export function useSendNotifyFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '模板内容',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'templateCode',
|
||||
label: '模板编码',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
},
|
||||
rules: z.number().default(UserTypeEnum.MEMBER),
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '接收人ID',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
dependencies: {
|
||||
show(values) {
|
||||
return values.userType === UserTypeEnum.MEMBER;
|
||||
},
|
||||
triggerFields: ['userType'],
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '接收人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择接收人',
|
||||
},
|
||||
dependencies: {
|
||||
show(values) {
|
||||
return values.userType === UserTypeEnum.ADMIN;
|
||||
},
|
||||
triggerFields: ['userType'],
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'templateParams',
|
||||
label: '模板参数',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemNotifyTemplateApi.NotifyTemplate>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '模板名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '模板编码',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '发送人名称',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '模板内容',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '模板类型',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '站内信模板',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:notify-template:update']),
|
||||
},
|
||||
{
|
||||
code: 'send',
|
||||
text: '测试',
|
||||
show: hasAccessByCodes(['system:notify-template:send-notify']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:notify-template:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteNotifyTemplate,
|
||||
exportNotifyTemplate,
|
||||
getNotifyTemplatePage,
|
||||
} from '#/api/system/notify/template';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
import SendForm from './modules/send-form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [SendModal, sendModalApi] = useVbenModal({
|
||||
connectedComponent: SendForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportNotifyTemplate(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '站内信模板.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建站内信模板 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑站内信模板 */
|
||||
function onEdit(row: SystemNotifyTemplateApi.NotifyTemplate) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 发送测试站内信 */
|
||||
function onSend(row: SystemNotifyTemplateApi.NotifyTemplate) {
|
||||
sendModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除站内信模板 */
|
||||
async function onDelete(row: SystemNotifyTemplateApi.NotifyTemplate) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteNotifyTemplate(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemNotifyTemplateApi.NotifyTemplate>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'send': {
|
||||
onSend(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getNotifyTemplatePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemNotifyTemplateApi.NotifyTemplate>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="短信配置" url="https://doc.iocoder.cn/sms/" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<SendModal />
|
||||
<Grid table-title="站内信模板列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:notify-template:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['站内信模板']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:notify-template:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createNotifyTemplate,
|
||||
getNotifyTemplate,
|
||||
updateNotifyTemplate,
|
||||
} from '#/api/system/notify/template';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||
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 SystemNotifyTemplateApi.NotifyTemplate;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateNotifyTemplate(data)
|
||||
: createNotifyTemplate(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<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getNotifyTemplate(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemNotifyTemplateApi } from '#/api/system/notify/template';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { sendNotify } from '#/api/system/notify/template';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useSendNotifyFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 构建发送请求
|
||||
const values = await formApi.getValues();
|
||||
const paramsObj: Record<string, string> = {};
|
||||
if (formData.value?.params) {
|
||||
formData.value.params.forEach((param) => {
|
||||
paramsObj[param] = values[`param_${param}`];
|
||||
});
|
||||
}
|
||||
const data: SystemNotifyTemplateApi.NotifySendReqVO = {
|
||||
userId: values.userId,
|
||||
userType: values.userType,
|
||||
templateCode: formData.value?.code || '',
|
||||
templateParams: paramsObj,
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
try {
|
||||
await sendNotify(data);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} catch (error) {
|
||||
console.error('发送站内信失败', error);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 获取数据
|
||||
const data = modalApi.getData<SystemNotifyTemplateApi.NotifyTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
formData.value = data;
|
||||
// 更新 form schema
|
||||
const schema = buildFormSchema();
|
||||
formApi.setState({ schema });
|
||||
// 设置到 values
|
||||
await formApi.setValues({
|
||||
content: data.content,
|
||||
templateCode: data.code,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/** 动态构建表单 schema */
|
||||
const buildFormSchema = () => {
|
||||
const schema = useSendNotifyFormSchema();
|
||||
if (formData.value?.params) {
|
||||
formData.value.params.forEach((param: string) => {
|
||||
schema.push({
|
||||
fieldName: `param_${param}`,
|
||||
label: `参数 ${param}`,
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: `请输入参数 ${param}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
return schema;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="测试发送站内信">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'clientId',
|
||||
label: '客户端编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户端编号',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'secret',
|
||||
label: '客户端密钥',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户端密钥',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '应用名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用名',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'logo',
|
||||
label: '应用图标',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
limit: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '应用描述',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用描述',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'accessTokenValiditySeconds',
|
||||
label: '访问令牌的有效期',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入访问令牌的有效期,单位:秒',
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'refreshTokenValiditySeconds',
|
||||
label: '刷新令牌的有效期',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入刷新令牌的有效期,单位:秒',
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'authorizedGrantTypes',
|
||||
label: '授权类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE),
|
||||
mode: 'multiple',
|
||||
placeholder: '请输入授权类型',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'scopes',
|
||||
label: '授权范围',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请输入授权范围',
|
||||
mode: 'tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'autoApproveScopes',
|
||||
label: '自动授权范围',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请输入自动授权范围',
|
||||
mode: 'multiple',
|
||||
// TODO @芋艿:根据权限,自动授权范围
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'redirectUris',
|
||||
label: '可重定向的 URI 地址',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请输入可重定向的 URI 地址',
|
||||
mode: 'tags',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'authorities',
|
||||
label: '权限',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请输入权限',
|
||||
mode: 'tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'resourceIds',
|
||||
label: '资源',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
placeholder: '请输入资源',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'additionalInformation',
|
||||
label: '附加信息',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入附加信息,JSON 格式数据',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '应用名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入应用名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
allowClear: true,
|
||||
placeholder: '请输入状态',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemOAuth2ClientApi.OAuth2Client>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'clientId',
|
||||
title: '客户端编号',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'secret',
|
||||
title: '客户端密钥',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '应用名',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'logo',
|
||||
title: '应用图标',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'accessTokenValiditySeconds',
|
||||
title: '访问令牌的有效期',
|
||||
minWidth: 130,
|
||||
formatter: ({ cellValue }) => `${cellValue} 秒`,
|
||||
},
|
||||
{
|
||||
field: 'refreshTokenValiditySeconds',
|
||||
title: '刷新令牌的有效期',
|
||||
minWidth: 130,
|
||||
formatter: ({ cellValue }) => `${cellValue} 秒`,
|
||||
},
|
||||
{
|
||||
field: 'authorizedGrantTypes',
|
||||
title: '授权类型',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: 'OAuth2 客户端',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:oauth2-client:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:oauth2-client:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteOAuth2Client,
|
||||
getOAuth2ClientPage,
|
||||
} from '#/api/system/oauth2/client';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建 OAuth2 客户端 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑 OAuth2 客户端 */
|
||||
function onEdit(row: SystemOAuth2ClientApi.OAuth2Client) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除 OAuth2 客户端 */
|
||||
async function onDelete(row: SystemOAuth2ClientApi.OAuth2Client) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteOAuth2Client(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemOAuth2ClientApi.OAuth2Client>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOAuth2ClientPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemOAuth2ClientApi.OAuth2Client>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="OAuth 2.0(SSO 单点登录)"
|
||||
url="https://doc.iocoder.cn/oauth2/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="OAuth2 客户端列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:oauth2-client:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [' OAuth2.0 客户端']) }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemOAuth2ClientApi } from '#/api/system/oauth2/client';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createOAuth2Client,
|
||||
getOAuth2Client,
|
||||
updateOAuth2Client,
|
||||
} from '#/api/system/oauth2/client';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemOAuth2ClientApi.OAuth2Client>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [' OAuth2.0 客户端'])
|
||||
: $t('ui.actionTitle.create', [' OAuth2.0 客户端']);
|
||||
});
|
||||
|
||||
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 SystemOAuth2ClientApi.OAuth2Client;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateOAuth2Client(data)
|
||||
: createOAuth2Client(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<SystemOAuth2ClientApi.OAuth2Client>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getOAuth2Client(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemOAuth2TokenApi } from '#/api/system/oauth2/token';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userType',
|
||||
label: '用户类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.USER_TYPE, 'number'),
|
||||
placeholder: '请选择用户类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'clientId',
|
||||
label: '客户端编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入客户端编号',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemOAuth2TokenApi.OAuth2Token>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'accessToken',
|
||||
title: '访问令牌',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'refreshToken',
|
||||
title: '刷新令牌',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userType',
|
||||
title: '用户类型',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.USER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'clientId',
|
||||
title: '客户端编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'expiresTime',
|
||||
title: '过期时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'accessToken',
|
||||
nameTitle: 'OAuth2 令牌',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'delete',
|
||||
text: '强退',
|
||||
show: hasAccessByCodes(['system:oauth2-token:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemOAuth2TokenApi } from '#/api/system/oauth2/token';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteOAuth2Token,
|
||||
getOAuth2TokenPage,
|
||||
} from '#/api/system/oauth2/token';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 删除 OAuth2 令牌 */
|
||||
async function onDelete(row: SystemOAuth2TokenApi.OAuth2Token) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', ['令牌']),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteOAuth2Token(row.accessToken);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemOAuth2TokenApi.OAuth2Token>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOAuth2TokenPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemOAuth2TokenApi.OAuth2Token>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="OAuth 2.0(SSO 单点登录)"
|
||||
url="https://doc.iocoder.cn/oauth2/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="令牌列表" />
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '操作人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
allowClear: true,
|
||||
placeholder: '请选择操作人员',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '操作模块',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入操作模块',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'subType',
|
||||
label: '操作名',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入操作名',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'action',
|
||||
label: '操作内容',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入操作内容',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '操作时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'bizId',
|
||||
label: '业务编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入业务编号',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemOperateLogApi.OperateLog>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '日志编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
title: '操作人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '操作模块',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'subType',
|
||||
title: '操作名',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作内容',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '操作时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'bizId',
|
||||
title: '业务编号',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'userIp',
|
||||
title: '操作IP',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'action',
|
||||
nameTitle: '操作日志',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['system:operate-log:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { exportOperateLog, getOperateLogPage } from '#/api/system/operate-log';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportOperateLog(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '操作日志.xls', source: data });
|
||||
}
|
||||
|
||||
/** 查看操作日志详情 */
|
||||
function onDetail(row: SystemOperateLogApi.OperateLog) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemOperateLogApi.OperateLog>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOperateLogPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemOperateLogApi.OperateLog>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" />
|
||||
</template>
|
||||
|
||||
<DetailModal @success="onRefresh" />
|
||||
<Grid table-title="操作日志列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:operate-log:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
const formData = ref<SystemOperateLogApi.OperateLog>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemOperateLogApi.OperateLog>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = data;
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="操作日志详情"
|
||||
class="w-1/2"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<NDescriptions
|
||||
bordered
|
||||
:column="1"
|
||||
size="middle"
|
||||
class="mx-4"
|
||||
:label-style="{ width: '110px' }"
|
||||
>
|
||||
<NDescriptionsItem label="日志编号">
|
||||
{{ formData?.id }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="链路追踪" v-if="formData?.traceId">
|
||||
{{ formData?.traceId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作人编号">
|
||||
{{ formData?.userId }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作人名字">
|
||||
{{ formData?.userName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作人IP">
|
||||
{{ formData?.userIp }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作人UA">
|
||||
{{ formData?.userAgent }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作模块">
|
||||
{{ formData?.type }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作名">
|
||||
{{ formData?.subType }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作内容">
|
||||
{{ formData?.action }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="formData?.extra" label="操作拓展参数">
|
||||
{{ formData?.extra }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="请求URL">
|
||||
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="操作时间">
|
||||
{{ formatDateTime(formData?.createTime || '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="业务编号">
|
||||
{{ formData?.bizId }}
|
||||
</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemPostApi } from '#/api/system/post';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '岗位名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'code',
|
||||
label: '岗位编码',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '岗位状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '岗位备注',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '岗位名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '岗位编码',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '岗位状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemPostApi.Post>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '岗位编号',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '岗位名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '岗位编码',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示顺序',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '岗位备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '岗位状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '岗位',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:post:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:post:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemPostApi } from '#/api/system/post';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deletePost, exportPost, getPostPage } from '#/api/system/post';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportPost(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '岗位.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建岗位 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑岗位 */
|
||||
function onEdit(row: SystemPostApi.Post) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除岗位 */
|
||||
async function onDelete(row: SystemPostApi.Post) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deletePost(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<SystemPostApi.Post>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getPostPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemPostApi.Post>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="岗位列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:post:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['岗位']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:post:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemPostApi } from '#/api/system/post';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createPost, getPost, updatePost } from '#/api/system/post';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemPostApi.Post>();
|
||||
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 SystemPostApi.Post;
|
||||
try {
|
||||
await (formData.value?.id ? updatePost(data) : createPost(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<SystemPostApi.Post>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getPost(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[40%]" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
SystemDataScopeEnum,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '角色标识',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入显示顺序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '角色状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '角色备注',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 分配数据权限的表单 */
|
||||
export function useAssignDataPermissionFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'code',
|
||||
label: '角色标识',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'dataScope',
|
||||
label: '权限范围',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'dataScopeDeptIds',
|
||||
label: '部门范围',
|
||||
component: 'Input',
|
||||
formItemClass: 'items-start',
|
||||
dependencies: {
|
||||
triggerFields: ['dataScope'],
|
||||
show: (values) => {
|
||||
return values.dataScope === SystemDataScopeEnum.DEPT_CUSTOM;
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 分配菜单的表单 */
|
||||
export function useAssignMenuFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '角色标识',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'menuIds',
|
||||
label: '菜单权限',
|
||||
component: 'Input',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '角色名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '角色标识',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '角色状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = SystemRoleApi.Role>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '角色编号',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '角色名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '角色类型',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_ROLE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '角色标识',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示顺序',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '角色备注',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '角色状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '角色',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['system:role:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['system:role:delete']),
|
||||
},
|
||||
{
|
||||
code: 'assign-data-permission',
|
||||
text: '数据权限',
|
||||
show: hasAccessByCodes([
|
||||
'system:permission:assign-role-data-scope',
|
||||
]),
|
||||
},
|
||||
{
|
||||
code: 'assign-menu',
|
||||
text: '菜单权限',
|
||||
show: hasAccessByCodes(['system:permission:assign-role-menu']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Download, Plus } from '@vben/icons';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { NButton } from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteRole, exportRole, getRolePage } from '#/api/system/role';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import AssignDataPermissionForm from './modules/assign-data-permission-form.vue';
|
||||
import AssignMenuForm from './modules/assign-menu-form.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [AssignDataPermissionFormModel, assignDataPermissionFormApi] =
|
||||
useVbenModal({
|
||||
connectedComponent: AssignDataPermissionForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [AssignMenuFormModel, assignMenuFormApi] = useVbenModal({
|
||||
connectedComponent: AssignMenuForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function onExport() {
|
||||
const data = await exportRole(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '角色.xls', source: data });
|
||||
}
|
||||
|
||||
/** 编辑角色 */
|
||||
function onEdit(row: SystemRoleApi.Role) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 创建角色 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
async function onDelete(row: SystemRoleApi.Role) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteRole(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 分配角色的数据权限 */
|
||||
function onAssignDataPermission(row: SystemRoleApi.Role) {
|
||||
assignDataPermissionFormApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 分配角色的菜单权限 */
|
||||
function onAssignMenu(row: SystemRoleApi.Role) {
|
||||
assignMenuFormApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<SystemRoleApi.Role>) {
|
||||
switch (code) {
|
||||
case 'assign-data-permission': {
|
||||
onAssignDataPermission(row);
|
||||
break;
|
||||
}
|
||||
case 'assign-menu': {
|
||||
onAssignMenu(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getRolePage({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemRoleApi.Role>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="功能权限"
|
||||
url="https://doc.iocoder.cn/resource-permission"
|
||||
/>
|
||||
<DocAlert title="数据权限" url="https://doc.iocoder.cn/data-permission" />
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<AssignDataPermissionFormModel @success="onRefresh" />
|
||||
<AssignMenuFormModel @success="onRefresh" />
|
||||
<Grid table-title="角色列表">
|
||||
<template #toolbar-tools>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['system:role:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['角色']) }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
@click="onExport"
|
||||
v-access:code="['system:role:export']"
|
||||
>
|
||||
<Download class="size-5" />
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenTree } from '@vben/common-ui';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { NCheckbox } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getDeptList } from '#/api/system/dept';
|
||||
import { assignRoleDataScope } from '#/api/system/permission';
|
||||
import { getRole } from '#/api/system/role';
|
||||
import { $t } from '#/locales';
|
||||
import { SystemDataScopeEnum } from '#/utils';
|
||||
|
||||
import { useAssignDataPermissionFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const deptTree = ref<SystemDeptApi.Dept[]>([]); // 部门树
|
||||
const deptLoading = ref(false); // 加载部门列表
|
||||
const isAllSelected = ref(false); // 全选状态
|
||||
const isExpanded = ref(false); // 展开状态
|
||||
const isCheckStrictly = ref(true); // 父子联动状态
|
||||
const expandedKeys = ref<number[]>([]); // 展开的节点
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useAssignDataPermissionFormSchema(),
|
||||
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 assignRoleDataScope({
|
||||
roleId: data.id,
|
||||
dataScope: data.dataScope,
|
||||
dataScopeDeptIds:
|
||||
data.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
|
||||
? data.dataScopeDeptIds
|
||||
: undefined,
|
||||
});
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<SystemRoleApi.Role>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await formApi.setValues(await getRole(data.id as number));
|
||||
|
||||
// 加载部门列表
|
||||
await loadDeptTree();
|
||||
toggleExpandAll();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 加载部门树 */
|
||||
async function loadDeptTree() {
|
||||
deptLoading.value = true;
|
||||
try {
|
||||
const data = await getDeptList();
|
||||
deptTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||
} finally {
|
||||
deptLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 全选/全不选 */
|
||||
function toggleSelectAll() {
|
||||
isAllSelected.value = !isAllSelected.value;
|
||||
if (isAllSelected.value) {
|
||||
const allIds = getAllNodeIds(deptTree.value);
|
||||
formApi.setFieldValue('dataScopeDeptIds', allIds);
|
||||
} else {
|
||||
formApi.setFieldValue('dataScopeDeptIds', []);
|
||||
}
|
||||
}
|
||||
|
||||
/** 展开/折叠所有节点 */
|
||||
function toggleExpandAll() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
// 获取所有节点的 ID
|
||||
expandedKeys.value = isExpanded.value ? getAllNodeIds(deptTree.value) : [];
|
||||
}
|
||||
|
||||
/** 切换父子联动 */
|
||||
function toggleCheckStrictly() {
|
||||
isCheckStrictly.value = !isCheckStrictly.value;
|
||||
}
|
||||
|
||||
/** 递归获取所有节点 ID */
|
||||
function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
||||
nodes.forEach((node: any) => {
|
||||
ids.push(node.id);
|
||||
if (node.children && node.children.length > 0) {
|
||||
getAllNodeIds(node.children, ids);
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="数据权限" class="w-[40%]">
|
||||
<Form class="mx-4">
|
||||
<template #dataScopeDeptIds="slotProps">
|
||||
<!-- <Spin :spinning="deptLoading"> -->
|
||||
<!-- TODO @芋艿:可优化,使用 antd 的 tree?原因是,更原生 -->
|
||||
<VbenTree
|
||||
:tree-data="deptTree"
|
||||
multiple
|
||||
bordered
|
||||
:expanded="expandedKeys"
|
||||
v-bind="slotProps"
|
||||
value-field="id"
|
||||
label-field="name"
|
||||
:auto-check-parent="false"
|
||||
:check-strictly="!isCheckStrictly"
|
||||
/>
|
||||
<!-- </Spin> -->
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<NCheckbox :checked="isAllSelected" @change="toggleSelectAll">
|
||||
全选
|
||||
</NCheckbox>
|
||||
<NCheckbox :checked="isExpanded" @change="toggleExpandAll">
|
||||
全部展开
|
||||
</NCheckbox>
|
||||
<NCheckbox :checked="isCheckStrictly" @change="toggleCheckStrictly">
|
||||
父子联动
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenTree } from '@vben/common-ui';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { NCheckbox } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getMenuList } from '#/api/system/menu';
|
||||
import { assignRoleMenu, getRoleMenuList } from '#/api/system/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useAssignMenuFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const menuTree = ref<SystemDeptApi.Dept[]>([]); // 菜单树
|
||||
const menuLoading = ref(false); // 加载菜单列表
|
||||
const isAllSelected = ref(false); // 全选状态
|
||||
const isExpanded = ref(false); // 展开状态
|
||||
const expandedKeys = ref<number[]>([]); // 展开的节点
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useAssignMenuFormSchema(),
|
||||
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 assignRoleMenu({
|
||||
roleId: data.id,
|
||||
menuIds: data.menuIds,
|
||||
});
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<SystemRoleApi.Role>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await formApi.setValues(data);
|
||||
|
||||
// 加载角色菜单
|
||||
const menuIds = await getRoleMenuList(data.id as number);
|
||||
await formApi.setFieldValue('menuIds', menuIds);
|
||||
// 加载菜单列表
|
||||
await loadMenuTree();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 加载菜单树 */
|
||||
async function loadMenuTree() {
|
||||
menuLoading.value = true;
|
||||
try {
|
||||
const data = await getMenuList();
|
||||
menuTree.value = handleTree(data) as SystemDeptApi.Dept[];
|
||||
} finally {
|
||||
menuLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 全选/全不选 */
|
||||
function toggleSelectAll() {
|
||||
isAllSelected.value = !isAllSelected.value;
|
||||
if (isAllSelected.value) {
|
||||
const allIds = getAllNodeIds(menuTree.value);
|
||||
formApi.setFieldValue('menuIds', allIds);
|
||||
} else {
|
||||
formApi.setFieldValue('menuIds', []);
|
||||
}
|
||||
}
|
||||
|
||||
/** 展开/折叠所有节点 */
|
||||
function toggleExpandAll() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
// 获取所有节点的 ID
|
||||
expandedKeys.value = isExpanded.value ? getAllNodeIds(menuTree.value) : [];
|
||||
}
|
||||
|
||||
/** 递归获取所有节点 ID */
|
||||
function getAllNodeIds(nodes: any[], ids: number[] = []): number[] {
|
||||
nodes.forEach((node: any) => {
|
||||
ids.push(node.id);
|
||||
if (node.children && node.children.length > 0) {
|
||||
getAllNodeIds(node.children, ids);
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="数据权限" class="w-[40%]">
|
||||
<Form class="mx-4">
|
||||
<template #menuIds="slotProps">
|
||||
<!-- <Spin :spinning="menuLoading" class="w-full"> -->
|
||||
<!-- TODO @芋艿:可优化,使用 antd 的 tree?原因是,更原生 -->
|
||||
<VbenTree
|
||||
:tree-data="menuTree"
|
||||
multiple
|
||||
bordered
|
||||
:expanded="expandedKeys"
|
||||
v-bind="slotProps"
|
||||
value-field="id"
|
||||
label-field="name"
|
||||
/>
|
||||
<!-- </Spin> -->
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<NCheckbox :checked="isAllSelected" @change="toggleSelectAll">
|
||||
全选
|
||||
</NCheckbox>
|
||||
<NCheckbox :checked="isExpanded" @change="toggleExpandAll">
|
||||
全部展开
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { createRole, getRole, updateRole } from '#/api/system/role';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemRoleApi.Role>();
|
||||
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 SystemRoleApi.Role;
|
||||
try {
|
||||
await (formData.value?.id ? updateRole(data) : createRole(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<SystemRoleApi.Role>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getRole(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue