!75 refactor: 代码生成优化

Merge pull request !75 from puhui999/v-next
pull/76/head
芋道源码 2025-04-19 01:34:56 +00:00 committed by Gitee
commit 3ff60f9690
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
30 changed files with 1913 additions and 294 deletions

View File

@ -0,0 +1,76 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace Demo03StudentApi {
/** 学生课程信息 */
export interface Demo03Course {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
score?: number; // 分数
}
/** 学生班级信息 */
export interface Demo03Grade {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
teacher?: string; // 班主任
}
/** 学生信息 */
export interface Demo03Student {
id: number; // 编号
name?: string; // 名字
sex?: number; // 性别
birthday?: Date; // 出生日期
description?: string; // 简介
demo03courses?: Demo03Course[];
demo03grade?: Demo03Grade;
}
}
/** 查询学生分页 */
export function getDemo03StudentPage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>('/infra/demo03-student/page', { params });
}
/** 查询学生详情 */
export function getDemo03Student(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Student>(`/infra/demo03-student/get?id=${id}`);
}
/** 新增学生 */
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.post('/infra/demo03-student/create', data);
}
/** 修改学生 */
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.put('/infra/demo03-student/update', data);
}
/** 删除学生 */
export function deleteDemo03Student(id: number) {
return requestClient.delete(`/infra/demo03-student/delete?id=${id}`);
}
/** 导出学生 */
export function exportDemo03Student(params: any) {
return requestClient.download('/infra/demo03-student/export-excel', params);
}
// ==================== 子表(学生课程) ====================
/** 获得学生课程列表 */
export function getDemo03CourseListByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Course[]>(
`/infra/demo03-student/demo03-course/list-by-student-id?studentId=${studentId}`,
);
}
// ==================== 子表(学生班级) ====================
/** 获得学生班级 */
export function getDemo03GradeByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Grade>(
`/infra/demo03-student/demo03-grade/get-by-student-id?studentId=${studentId}`,
);
}

View File

@ -0,0 +1,76 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace Demo03StudentApi {
/** 学生课程信息 */
export interface Demo03Course {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
score?: number; // 分数
}
/** 学生班级信息 */
export interface Demo03Grade {
id: number; // 编号
studentId?: number; // 学生编号
name?: string; // 名字
teacher?: string; // 班主任
}
/** 学生信息 */
export interface Demo03Student {
id: number; // 编号
name?: string; // 名字
sex?: number; // 性别
birthday?: Date; // 出生日期
description?: string; // 简介
demo03courses?: Demo03Course[];
demo03grade?: Demo03Grade;
}
}
/** 查询学生分页 */
export function getDemo03StudentPage(params: PageParam) {
return requestClient.get<PageResult<Demo03StudentApi.Demo03Student>>('/infra/demo03-student/page', { params });
}
/** 查询学生详情 */
export function getDemo03Student(id: number) {
return requestClient.get<Demo03StudentApi.Demo03Student>(`/infra/demo03-student/get?id=${id}`);
}
/** 新增学生 */
export function createDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.post('/infra/demo03-student/create', data);
}
/** 修改学生 */
export function updateDemo03Student(data: Demo03StudentApi.Demo03Student) {
return requestClient.put('/infra/demo03-student/update', data);
}
/** 删除学生 */
export function deleteDemo03Student(id: number) {
return requestClient.delete(`/infra/demo03-student/delete?id=${id}`);
}
/** 导出学生 */
export function exportDemo03Student(params: any) {
return requestClient.download('/infra/demo03-student/export-excel', params);
}
// ==================== 子表(学生课程) ====================
/** 获得学生课程列表 */
export function getDemo03CourseListByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Course[]>(
`/infra/demo03-student/demo03-course/list-by-student-id?studentId=${studentId}`,
);
}
// ==================== 子表(学生班级) ====================
/** 获得学生班级 */
export function getDemo03GradeByStudentId(studentId: number) {
return requestClient.get<Demo03StudentApi.Demo03Grade>(
`/infra/demo03-student/demo03-grade/get-by-student-id?studentId=${studentId}`,
);
}

View File

@ -27,7 +27,7 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/codegen/edit', path: '/codegen/edit',
name: 'InfraCodegenEdit', name: 'InfraCodegenEdit',
component: () => import('#/views/infra/codegen/edit.vue'), component: () => import('#/views/infra/codegen/edit/index.vue'),
meta: { meta: {
title: '修改生成配置', title: '修改生成配置',
activeMenu: '/infra/codegen', activeMenu: '/infra/codegen',

View File

@ -1,13 +1,13 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen'; import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import type { SystemMenuApi } from '#/api/system/menu'; import type { SystemMenuApi } from '#/api/system/menu';
import type { Recordable } from '@vben/types'; import type { Recordable } from '@vben/types';
import type { ComputedRef } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { z } from '#/adapter/form'; import { getDataSourceConfigList } from '#/api/infra/data-source-config';
import { getMenuList } from '#/api/system/menu'; import { getMenuList } from '#/api/system/menu';
import { getRangePickerDefaultProps } from '#/utils/date'; import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict'; import { DICT_TYPE, getDictOptions } from '#/utils/dict';
@ -20,24 +20,24 @@ import { $t } from '@vben/locales';
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
/** 导入数据库表的表单 */ /** 导入数据库表的表单 */
export function useImportTableFormSchema( export function useImportTableFormSchema(): VbenFormSchema[] {
dataSourceConfigList: InfraDataSourceConfigApi.InfraDataSourceConfig[],
): VbenFormSchema[] {
return [ return [
{ {
fieldName: 'dataSourceConfigId', fieldName: 'dataSourceConfigId',
label: '数据源', label: '数据源',
// TODO @puhui999不确定使用 ApiSelect 的话,使用 afterEach可以设置默认 defaultValue 不 component: 'ApiSelect',
component: 'Select',
componentProps: { componentProps: {
options: dataSourceConfigList.map((item) => ({ api: async () => {
label: item.name, const data = await getDataSourceConfigList();
value: item.id, return data.map((item) => ({
})), label: item.name,
value: item.id,
}));
},
autoSelect: 'first',
placeholder: '请选择数据源', placeholder: '请选择数据源',
}, },
defaultValue: dataSourceConfigList[0]?.id, rules: 'selectRequired',
rules: 'required',
}, },
{ {
fieldName: 'name', fieldName: 'name',
@ -60,6 +60,15 @@ export function useImportTableFormSchema(
]; ];
} }
/** 导入数据库表表格列定义 */
export function useImportTableColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 40 },
{ field: 'name', title: '表名称', minWidth: 200 },
{ field: 'comment', title: '表描述', minWidth: 200 },
];
}
/** 基本信息表单的 schema */ /** 基本信息表单的 schema */
export function useBasicInfoFormSchema(): VbenFormSchema[] { export function useBasicInfoFormSchema(): VbenFormSchema[] {
return [ return [
@ -124,7 +133,7 @@ export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE, 'number'), options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE, 'number'),
class: 'w-full', class: 'w-full',
}, },
rules: z.number().min(1, { message: '生成模板不能为空' }), rules: 'selectRequired',
}, },
{ {
component: 'Select', component: 'Select',
@ -134,8 +143,7 @@ export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE, 'number'), options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE, 'number'),
class: 'w-full', class: 'w-full',
}, },
// todo @puhui9991 可以是枚举么 rules: 'selectRequired',
rules: z.number().min(1, { message: '前端类型不能为空' }),
}, },
{ {
component: 'Select', component: 'Select',
@ -145,8 +153,7 @@ export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE, 'number'), options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE, 'number'),
class: 'w-full', class: 'w-full',
}, },
// todo @puhui9991 可以是枚举么 rules: 'selectRequired',
rules: z.number().min(1, { message: '生成场景不能为空' }),
}, },
{ {
fieldName: 'parentMenuId', fieldName: 'parentMenuId',
@ -199,36 +206,34 @@ export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
fieldName: 'moduleName', fieldName: 'moduleName',
label: '模块名', label: '模块名',
help: '模块名,即一级目录,例如 system、infra、tool 等等', help: '模块名,即一级目录,例如 system、infra、tool 等等',
// TODO @puhui999这种 rules可以使用 required rules: 'required',
rules: z.string().min(1, { message: '模块名不能为空' }),
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'businessName', fieldName: 'businessName',
label: '业务名', label: '业务名',
help: '业务名,即二级目录,例如 user、permission、dict 等等', help: '业务名,即二级目录,例如 user、permission、dict 等等',
rules: z.string().min(1, { message: '业务名不能为空' }), rules: 'required',
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'className', fieldName: 'className',
label: '类名称', label: '类名称',
help: '类名称首字母大写例如SysUser、SysMenu、SysDictData 等等', help: '类名称首字母大写例如SysUser、SysMenu、SysDictData 等等',
rules: z.string().min(1, { message: '类名称不能为空' }), rules: 'required',
}, },
{ {
component: 'Input', component: 'Input',
fieldName: 'classComment', fieldName: 'classComment',
label: '类描述', label: '类描述',
help: '用作类描述,例如 用户', help: '用作类描述,例如 用户',
rules: z.string().min(1, { message: '类描述不能为空' }), rules: 'required',
}, },
]; ];
} }
// TODO @puhui999是不是使用 useGenerationInfoTreeFormSchema主要考虑对称
/** 树表信息 schema */ /** 树表信息 schema */
export function useTreeTableFormSchema(columns: InfraCodegenApi.CodegenColumn[] = []): VbenFormSchema[] { export function useGenerationInfoTreeFormSchema(columns: InfraCodegenApi.CodegenColumn[] = []): VbenFormSchema[] {
return [ return [
{ {
component: 'Divider', component: 'Divider',
@ -276,9 +281,8 @@ export function useTreeTableFormSchema(columns: InfraCodegenApi.CodegenColumn[]
]; ];
} }
// TODO @puhui999【类似】是不是使用 useGenerationInfoTreeFormSchema主要考虑对称
/** 主子表信息 schema */ /** 主子表信息 schema */
export function useSubTableFormSchema( export function useGenerationInfoSubTableFormSchema(
columns: InfraCodegenApi.CodegenColumn[] = [], columns: InfraCodegenApi.CodegenColumn[] = [],
tables: InfraCodegenApi.CodegenTable[] = [], tables: InfraCodegenApi.CodegenTable[] = [],
): VbenFormSchema[] { ): VbenFormSchema[] {
@ -387,17 +391,14 @@ export function useGridFormSchema(): VbenFormSchema[] {
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = InfraCodegenApi.CodegenTable>( export function useGridColumns<T = InfraCodegenApi.CodegenTable>(
onActionClick: OnActionClickFn<T>, onActionClick: OnActionClickFn<T>,
dataSourceConfigList: InfraDataSourceConfigApi.InfraDataSourceConfig[], getDataSourceConfigName: ComputedRef<(cellValue: number) => string>,
): VxeTableGridOptions['columns'] { ): VxeTableGridOptions['columns'] {
return [ return [
{ {
field: 'dataSourceConfigId', field: 'dataSourceConfigId',
title: '数据源', title: '数据源',
minWidth: 120, minWidth: 120,
formatter: ({ cellValue }) => { formatter: ({ cellValue }) => getDataSourceConfigName.value(cellValue),
const config = dataSourceConfigList.find((item) => item.id === cellValue);
return config ? config.name : '';
},
}, },
{ {
field: 'tableName', field: 'tableName',

View File

@ -1,10 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
// TODO @puhui999 edit
import type { InfraCodegenApi } from '#/api/infra/codegen'; import type { InfraCodegenApi } from '#/api/infra/codegen';
import BasicInfo from './modules/basic-info.vue'; import BasicInfo from '../modules/basic-info.vue';
import ColumnInfo from './modules/column-info.vue'; import ColumnInfo from '../modules/column-info.vue';
import GenerationInfo from './modules/generation-info.vue'; import GenerationInfo from '../modules/generation-info.vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { ChevronsLeft } from '@vben/icons'; import { ChevronsLeft } from '@vben/icons';
import { Button, message, Steps } from 'ant-design-vue'; import { Button, message, Steps } from 'ant-design-vue';
@ -82,7 +81,6 @@ const submitForm = async () => {
} }
}; };
// TODO @puhui999
/** 返回列表 */ /** 返回列表 */
const close = () => { const close = () => {
router.push('/infra/codegen'); router.push('/infra/codegen');
@ -142,11 +140,6 @@ getDetail();
<Button v-show="currentStep === steps.length - 1" type="primary" :loading="loading" @click="submitForm"> <Button v-show="currentStep === steps.length - 1" type="primary" :loading="loading" @click="submitForm">
保存 保存
</Button> </Button>
<!-- TODO @puhui999返回要不去掉感觉一般自己点击关闭就好啦 -->
<Button @click="close">
<ChevronsLeft class="mr-1" />
返回
</Button>
</div> </div>
</div> </div>
</Page> </Page>

View File

@ -14,7 +14,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteCodegenTable, downloadCodegen, getCodegenTablePage, syncCodegenFromDB } from '#/api/infra/codegen'; import { deleteCodegenTable, downloadCodegen, getCodegenTablePage, syncCodegenFromDB } from '#/api/infra/codegen';
import { getDataSourceConfigList } from '#/api/infra/data-source-config'; import { getDataSourceConfigList } from '#/api/infra/data-source-config';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@ -22,6 +22,9 @@ import { useRouter } from 'vue-router';
const router = useRouter(); const router = useRouter();
const dataSourceConfigList = ref<InfraDataSourceConfigApi.InfraDataSourceConfig[]>([]); const dataSourceConfigList = ref<InfraDataSourceConfigApi.InfraDataSourceConfig[]>([]);
const getDataSourceConfigName = computed(
() => (cellValue: number) => dataSourceConfigList.value.find((item) => item.id === cellValue)?.name || '',
);
const [ImportModal, importModalApi] = useVbenModal({ const [ImportModal, importModalApi] = useVbenModal({
connectedComponent: ImportTable, connectedComponent: ImportTable,
@ -50,8 +53,7 @@ function onPreview(row: InfraCodegenApi.CodegenTable) {
/** 编辑表格 */ /** 编辑表格 */
function onEdit(row: InfraCodegenApi.CodegenTable) { function onEdit(row: InfraCodegenApi.CodegenTable) {
// TODO @puhui999使 name router.push({ name: 'InfraCodegenEdit', query: { id: row.id } });
router.push(`/codegen/edit?id=${row.id}`);
} }
/** 删除代码生成配置 */ /** 删除代码生成配置 */
@ -120,14 +122,14 @@ async function onGenerate(row: InfraCodegenApi.CodegenTable) {
/** 表格操作按钮的回调函数 */ /** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<InfraCodegenApi.CodegenTable>) { function onActionClick({ code, row }: OnActionClickParams<InfraCodegenApi.CodegenTable>) {
switch (code) { switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': { case 'delete': {
onDelete(row); onDelete(row);
break; break;
} }
case 'edit': {
onEdit(row);
break;
}
case 'generate': { case 'generate': {
onGenerate(row); onGenerate(row);
break; break;
@ -148,7 +150,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick, dataSourceConfigList.value), columns: useGridColumns(onActionClick, getDataSourceConfigName),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@ -172,14 +174,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<InfraCodegenApi.CodegenTable>, } as VxeTableGridOptions<InfraCodegenApi.CodegenTable>,
}); });
// TODO @puhui999使 apiselect
/** 获取数据源配置列表 */ /** 获取数据源配置列表 */
async function initDataSourceConfig() { async function initDataSourceConfig() {
try { try {
dataSourceConfigList.value = await getDataSourceConfigList(); dataSourceConfigList.value = await getDataSourceConfigList();
gridApi.setState({
gridOptions: { columns: useGridColumns(onActionClick, dataSourceConfigList.value) },
});
} catch (error) { } catch (error) {
console.error('获取数据源配置失败', error); console.error('获取数据源配置失败', error);
} }

View File

@ -8,7 +8,11 @@ import { computed, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useGenerationInfoBaseFormSchema, useSubTableFormSchema, useTreeTableFormSchema } from '../data'; import {
useGenerationInfoBaseFormSchema,
useGenerationInfoSubTableFormSchema,
useGenerationInfoTreeFormSchema,
} from '../data';
const props = defineProps<{ const props = defineProps<{
columns?: InfraCodegenApi.CodegenColumn[]; columns?: InfraCodegenApi.CodegenColumn[];
@ -55,14 +59,14 @@ const [SubForm, subFormApi] = useVbenForm({
/** 更新树表信息表单 schema */ /** 更新树表信息表单 schema */
function updateTreeSchema(): void { function updateTreeSchema(): void {
treeFormApi.setState({ treeFormApi.setState({
schema: useTreeTableFormSchema(props.columns) schema: useGenerationInfoTreeFormSchema(props.columns),
}); });
} }
/** 更新主子表信息表单 schema */ /** 更新主子表信息表单 schema */
function updateSubSchema(): void { function updateSubSchema(): void {
subFormApi.setState({ subFormApi.setState({
schema: useSubTableFormSchema(props.columns, tables.value) schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value),
}); });
} }
@ -71,7 +75,6 @@ async function getAllFormValues(): Promise<Record<string, any>> {
// //
const baseValues = await baseFormApi.getValues(); const baseValues = await baseFormApi.getValues();
// //
// TODO @puhui999使
let extraValues = {}; let extraValues = {};
if (isTreeTable.value) { if (isTreeTable.value) {
extraValues = await treeFormApi.getValues(); extraValues = await treeFormApi.getValues();
@ -84,20 +87,18 @@ async function getAllFormValues(): Promise<Record<string, any>> {
/** 验证所有表单 */ /** 验证所有表单 */
async function validateAllForms() { async function validateAllForms() {
let validateResult: boolean;
// //
const { valid: baseFormValid } = await baseFormApi.validate(); const { valid: baseFormValid } = await baseFormApi.validate();
validateResult = baseFormValid;
// //
// TODO @puhui999 extraValid validateResult && extraValid let extraValid = true;
if (isTreeTable.value) { if (isTreeTable.value) {
const { valid: treeFormValid } = await treeFormApi.validate(); const { valid: treeFormValid } = await treeFormApi.validate();
validateResult = baseFormValid && treeFormValid; extraValid = treeFormValid;
} else if (isSubTable.value) { } else if (isSubTable.value) {
const { valid: subFormValid } = await subFormApi.validate(); const { valid: subFormValid } = await subFormApi.validate();
validateResult = baseFormValid && subFormValid; extraValid = subFormValid;
} }
return validateResult; return baseFormValid && extraValid;
} }
/** 设置表单值 */ /** 设置表单值 */
@ -126,15 +127,18 @@ watch(
if (!val || isEmpty(val)) { if (!val || isEmpty(val)) {
return; return;
} }
const table = val as InfraCodegenApi.CodegenTable;
// schema // schema
updateTreeSchema(); updateTreeSchema();
// //
setAllFormValues(val); setAllFormValues(table);
// //
if (typeof val.dataSourceConfigId === undefined) { const dataSourceConfigId = table.dataSourceConfigId;
if (dataSourceConfigId === undefined) {
return; return;
} }
tables.value = await getCodegenTableList(val.dataSourceConfigId); tables.value = await getCodegenTableList(dataSourceConfigId);
// schema // schema
updateSubSchema(); updateSubSchema();
}, },

View File

@ -1,55 +1,43 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen'; import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { createCodegenList, getSchemaTableList } from '#/api/infra/codegen'; import { createCodegenList, getSchemaTableList } from '#/api/infra/codegen';
import { getDataSourceConfigList } from '#/api/infra/data-source-config'; import { reactive } from 'vue';
import { reactive, ref, unref } from 'vue';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useImportTableFormSchema } from '#/views/infra/codegen/data'; import { useImportTableColumns, useImportTableFormSchema } from '#/views/infra/codegen/data';
/** 定义组件事件 */ /** 定义组件事件 */
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'success'): void; (e: 'success'): void;
}>(); }>();
const dataSourceConfigList = ref<InfraDataSourceConfigApi.InfraDataSourceConfig[]>([]);
const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({ const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
dataSourceConfigId: undefined, dataSourceConfigId: 0,
tableNames: [], // tableNames: [], //
}); });
/** 表格实例 */ /** 表格实例 */
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useImportTableFormSchema([]), schema: useImportTableFormSchema(),
submitOnChange: true,
}, },
gridOptions: { gridOptions: {
// TODO @puhui999 columns: useImportTableColumns(),
columns: [
{ type: 'checkbox', width: 40 },
{ field: 'name', title: '表名称', minWidth: 200 },
{ field: 'comment', title: '表描述', minWidth: 200 },
],
height: 600, height: 600,
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
// TODO @puhui999使 formValues.dataSourceConfigId
if (formValues.dataSourceConfigId === undefined) { if (formValues.dataSourceConfigId === undefined) {
if (unref(dataSourceConfigList).length > 0) { return [];
formValues.dataSourceConfigId = unref(dataSourceConfigList)[0]?.id;
} else {
return [];
}
} }
formData.dataSourceConfigId = formValues.dataSourceConfigId; formData.dataSourceConfigId = formValues.dataSourceConfigId;
return await getSchemaTableList({ return await getSchemaTableList({
@ -119,23 +107,6 @@ const [Modal, modalApi] = useVbenModal({
} }
}, },
}); });
/** 获取数据源配置列表 */
async function initDataSourceConfig() {
try {
dataSourceConfigList.value = await getDataSourceConfigList();
gridApi.setState({
formOptions: {
schema: useImportTableFormSchema(dataSourceConfigList.value),
},
});
} catch (error) {
console.error('获取数据源配置失败', error);
}
}
/** 初始化 */
initDataSourceConfig();
</script> </script>
<template> <template>

View File

@ -1,13 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { InfraCodegenApi } from '#/api/infra/codegen'; import type { InfraCodegenApi } from '#/api/infra/codegen';
import { computed, h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { Copy } from '@vben/icons'; import { Copy } from '@vben/icons';
import { Button, DirectoryTree, message, Tabs } from 'ant-design-vue';
import { previewCodegen } from '#/api/infra/codegen';
import { h, ref } from 'vue';
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { Button, message, Tree } from 'ant-design-vue';
import hljs from 'highlight.js/lib/core'; import hljs from 'highlight.js/lib/core';
import java from 'highlight.js/lib/languages/java'; import java from 'highlight.js/lib/languages/java';
import javascript from 'highlight.js/lib/languages/javascript'; import javascript from 'highlight.js/lib/languages/javascript';
@ -15,8 +16,6 @@ import sql from 'highlight.js/lib/languages/sql';
import typescript from 'highlight.js/lib/languages/typescript'; import typescript from 'highlight.js/lib/languages/typescript';
import xml from 'highlight.js/lib/languages/xml'; import xml from 'highlight.js/lib/languages/xml';
import { previewCodegen } from '#/api/infra/codegen';
/** 注册代码高亮语言 */ /** 注册代码高亮语言 */
hljs.registerLanguage('java', java); hljs.registerLanguage('java', java);
hljs.registerLanguage('xml', xml); hljs.registerLanguage('xml', xml);
@ -40,19 +39,36 @@ const loading = ref(false);
const fileTree = ref<FileNode[]>([]); const fileTree = ref<FileNode[]>([]);
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]); const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
const activeKey = ref<string>(''); const activeKey = ref<string>('');
const highlightedCode = ref<string>('');
/** 当前活动文件的语言 */ /** 代码地图 */
const activeLanguage = computed(() => { const codeMap = new Map<string, string>();
return activeKey.value.split('.').pop() || ''; const setCodeMode = (key: string, lang: string, code: string) => {
}); // Java
const trimmedCode = code.trimStart();
try {
const highlightedCode = hljs.highlight(trimmedCode, {
language: lang,
}).value;
codeMap.set(key, highlightedCode);
} catch {
codeMap.set(key, trimmedCode);
}
};
const removeCodeMapKey = (targetKey: any) => {
//
if (codeMap.size === 1) {
return;
}
if (codeMap.has(targetKey)) {
codeMap.delete(targetKey);
}
};
/** 复制代码 */ /** 复制代码 */
const copyCode = async () => { const copyCode = async () => {
const { copy } = useClipboard(); const { copy } = useClipboard();
const file = previewFiles.value.find( const file = previewFiles.value.find((item) => item.filePath === activeKey.value);
(item) => item.filePath === activeKey.value,
);
if (file) { if (file) {
await copy(file.code); await copy(file.code);
message.success('复制成功'); message.success('复制成功');
@ -61,27 +77,25 @@ const copyCode = async () => {
/** 文件节点点击事件 */ /** 文件节点点击事件 */
const handleNodeClick = (_: any[], e: any) => { const handleNodeClick = (_: any[], e: any) => {
// TODO @puhui999if return if (!e.node.isLeaf) return;
if (e.node.isLeaf) {
activeKey.value = e.node.key; activeKey.value = e.node.key;
const file = previewFiles.value.find( const file = previewFiles.value.find((item) => {
(item) => item.filePath === activeKey.value, const list = activeKey.value.split('.');
); // -
if (file) { if (list.length > 2) {
const lang = file.filePath.split('.').pop() || ''; const lang = list.pop();
try { return item.filePath === `${list.join('/')}.${lang}`;
highlightedCode.value = hljs.highlight(file.code, {
language: lang,
}).value;
} catch {
highlightedCode.value = file.code;
}
} }
} return item.filePath === activeKey.value;
});
if (!file) return;
const lang = file.filePath.split('.').pop() || '';
setCodeMode(activeKey.value, lang, file.code);
}; };
/** 处理文件树 */ /** 处理文件树 */
// TODO @puhui999 cursor = =
const handleFiles = (data: InfraCodegenApi.CodegenPreview[]): FileNode[] => { const handleFiles = (data: InfraCodegenApi.CodegenPreview[]): FileNode[] => {
const exists: Record<string, boolean> = {}; const exists: Record<string, boolean> = {};
const files: FileNode[] = []; const files: FileNode[] = [];
@ -89,58 +103,57 @@ const handleFiles = (data: InfraCodegenApi.CodegenPreview[]): FileNode[] => {
// //
for (const item of data) { for (const item of data) {
const paths = item.filePath.split('/'); const paths = item.filePath.split('/');
let cursor = 0;
let fullPath = ''; let fullPath = '';
// Java while (cursor < paths.length) {
const newPaths = []; const path = paths[cursor] || '';
let i = 0; const oldFullPath = fullPath;
while (i < paths.length) {
const path = paths[i];
if (path === 'java' && i + 1 < paths.length) { // Java
newPaths.push(path); if (path === 'java' && cursor + 1 < paths.length) {
fullPath = fullPath ? `${fullPath}/${path}` : path;
cursor++;
// //
let packagePath = ''; let packagePath = '';
i++; while (cursor < paths.length) {
while (i < paths.length) { const nextPath = paths[cursor] || '';
const nextPath = paths[i] || ''; if (['controller', 'convert', 'dal', 'dataobject', 'enums', 'mysql', 'service', 'vo'].includes(nextPath)) {
if (['controller','convert','dal','dataobject','enums','mysql','service','vo'].includes(nextPath)) {
break; break;
} }
packagePath = packagePath ? `${packagePath}.${nextPath}` : nextPath; packagePath = packagePath ? `${packagePath}.${nextPath}` : nextPath;
i++; cursor++;
} }
if (packagePath) { if (packagePath) {
newPaths.push(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; continue;
} }
newPaths.push(path); //
i++; fullPath = fullPath ? `${fullPath}/${path}` : path;
} if (!exists[fullPath]) {
exists[fullPath] = true;
// files.push({
for (let i = 0; i < newPaths.length; i++) { key: fullPath,
const oldFullPath = fullPath; title: path,
fullPath = parentKey: oldFullPath || '/',
fullPath.length === 0 isLeaf: cursor === paths.length - 1,
? newPaths[i] || '' });
: `${fullPath.replaceAll('.', '/')}/${newPaths[i]}`;
if (exists[fullPath]) {
continue;
} }
cursor++;
exists[fullPath] = true;
files.push({
key: fullPath,
title: newPaths[i] || '',
parentKey: oldFullPath || '/',
isLeaf: i === newPaths.length - 1,
});
} }
} }
@ -163,11 +176,8 @@ const [Modal, modalApi] = useVbenModal({
class: 'w-3/5', class: 'w-3/5',
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
// TODO @puhui999 //
previewFiles.value = []; codeMap.clear();
fileTree.value = [];
activeKey.value = '';
highlightedCode.value = '';
return; return;
} }
@ -186,13 +196,7 @@ const [Modal, modalApi] = useVbenModal({
activeKey.value = data[0]?.filePath || ''; activeKey.value = data[0]?.filePath || '';
const lang = activeKey.value.split('.').pop() || ''; const lang = activeKey.value.split('.').pop() || '';
const code = data[0]?.code || ''; const code = data[0]?.code || '';
try { setCodeMode(activeKey.value, lang, code);
highlightedCode.value = hljs.highlight(code, {
language: lang,
}).value;
} catch {
highlightedCode.value = code;
}
} }
} finally { } finally {
loading.value = false; loading.value = false;
@ -203,42 +207,27 @@ const [Modal, modalApi] = useVbenModal({
<template> <template>
<Modal title="代码预览"> <Modal title="代码预览">
<div class="h-full flex" v-loading="loading"> <div class="flex h-full" v-loading="loading">
<!-- 文件树 --> <!-- 文件树 -->
<div class="w-1/3 border-r border-gray-200 pr-4 dark:border-gray-700"> <div class="w-1/3 border-r border-gray-200 pr-4 dark:border-gray-700">
<!-- TODO @puhui999树默认展示 --> <DirectoryTree v-model:active-key="activeKey" @select="handleNodeClick" :tree-data="fileTree" />
<!-- TODO @puhui999默认节点点击可以展开 -->
<Tree
:selected-keys="[activeKey]"
:tree-data="fileTree"
@select="handleNodeClick"
/>
</div> </div>
<!-- 代码预览 --> <!-- 代码预览 -->
<!-- TODO @puhui999可以顶部有个 tab -->
<!-- TODO @puhui999貌似 java 的缩进不太对首行空了很长 -->
<div class="w-2/3 pl-4"> <div class="w-2/3 pl-4">
<div class="mb-2 flex justify-between"> <Tabs v-model:active-key="activeKey" hide-add type="editable-card" @edit="removeCodeMapKey">
<div class="text-lg font-medium dark:text-gray-200"> <Tabs.TabPane v-for="key in codeMap.keys()" :key="key" :tab="key.split('/').pop()">
{{ activeKey.split('/').pop() }} <div class="h-[calc(100%-40px)] overflow-auto">
<!-- TODO @puhui999貌似不用 activeLanguage --> <pre class="overflow-auto rounded-md bg-gray-50 p-4 text-gray-800 dark:bg-gray-800 dark:text-gray-200">
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400"> <!-- eslint-disable-next-line vue/no-v-html -->
({{ activeLanguage }}) <code v-html="codeMap.get(activeKey)" class="code-highlight"></code>
</span> </pre>
</div> </div>
<!-- TODO @芋艿貌似别的模块也可以通过 :icon="h(Copy)" --> </Tabs.TabPane>
<Button type="primary" ghost @click="copyCode" :icon="h(Copy)"> <template #rightExtra>
复制代码 <!-- TODO @芋艿貌似别的模块也可以通过 :icon="h(Copy)" -->
</Button> <Button type="primary" ghost @click="copyCode" :icon="h(Copy)"> 复制代码 </Button>
</div> </template>
<div class="h-[calc(100%-40px)] overflow-auto"> </Tabs>
<pre
class="overflow-auto rounded-md bg-gray-50 p-4 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<code v-html="highlightedCode" class="code-highlight"></code>
</pre>
</div>
</div> </div>
</div> </div>
</Modal> </Modal>
@ -249,9 +238,18 @@ const [Modal, modalApi] = useVbenModal({
/* 代码高亮样式 - 支持暗黑模式 */ /* 代码高亮样式 - 支持暗黑模式 */
:deep(.code-highlight) { :deep(.code-highlight) {
display: block;
white-space: pre;
background: transparent; background: transparent;
} }
/* 代码块内容无缩进 */
:deep(pre) {
padding: 1rem;
margin: 0;
white-space: pre;
}
/* 关键字 */ /* 关键字 */
:deep(.hljs-keyword) { :deep(.hljs-keyword) {
@apply text-purple-600 dark:text-purple-400; @apply text-purple-600 dark:text-purple-400;

View File

@ -1,6 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { getRangePickerDefaultProps } from '#/utils/date'; import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict'; import { DICT_TYPE, getDictOptions } from '#/utils/dict';
@ -105,9 +106,9 @@ export function useGridFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = Demo01ContactApi.Demo01Contact>( export function useGridColumns(
onActionClick: OnActionClickFn<T>, onActionClick?: OnActionClickFn<Demo01ContactApi.Demo01Contact>,
): VxeTableGridOptions['columns'] { ): VxeTableGridOptions<Demo01ContactApi.Demo01Contact>['columns'] {
return [ return [
{ {
field: 'id', field: 'id',
@ -153,9 +154,11 @@ export function useGridColumns<T = Demo01ContactApi.Demo01Contact>(
{ {
field: 'operation', field: 'operation',
title: '操作', title: '操作',
minWidth: 180, minWidth: 200,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: { cellRender: {
attrs: { attrs: {
nameField: 'id', nameField: 'id',

View File

@ -11,6 +11,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDemo01Contact, exportDemo01Contact, getDemo01ContactPage } from '#/api/infra/demo/demo01'; import { deleteDemo01Contact, exportDemo01Contact, getDemo01ContactPage } from '#/api/infra/demo/demo01';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { downloadByData } from '#/utils/download'; import { downloadByData } from '#/utils/download';
import { h } from 'vue';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@ -62,14 +63,14 @@ async function onDelete(row: Demo01ContactApi.Demo01Contact) {
/** 表格操作按钮的回调函数 */ /** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<Demo01ContactApi.Demo01Contact>) { function onActionClick({ code, row }: OnActionClickParams<Demo01ContactApi.Demo01Contact>) {
switch (code) { switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': { case 'delete': {
onDelete(row); onDelete(row);
break; break;
} }
case 'edit': {
onEdit(row);
break;
}
} }
} }
@ -80,7 +81,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(onActionClick),
height: 'auto', height: 'auto',
keepSource: true, pagerConfig: {
enabled: true,
},
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
@ -94,6 +97,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: { code: 'query' }, refresh: { code: 'query' },
@ -106,14 +110,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<FormModal @success="onRefresh" /> <FormModal @success="onRefresh" />
<Grid table-title=""> <Grid table-title="">
<template #toolbar-tools> <template #toolbar-tools>
<Button type="primary" @click="onCreate" v-access:code="['infra:demo01-contact:create']"> <Button :icon="h(Plus)" type="primary" @click="onCreate" v-access:code="['infra:demo01-contact:create']">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['示例联系人']) }} {{ $t('ui.actionTitle.create', ['示例联系人']) }}
</Button> </Button>
<Button type="primary" class="ml-2" @click="onExport" v-access:code="['infra:demo01-contact:export']"> <Button
<Download class="size-5" /> :icon="h(Download)"
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['infra:demo01-contact:export']"
>
{{ $t('ui.actionTitle.export') }} {{ $t('ui.actionTitle.export') }}
</Button> </Button>
</template> </template>

View File

@ -20,7 +20,7 @@ const getTitle = computed(() => {
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
layout: 'horizontal', layout: 'horizontal',
schema: useFormSchema(), schema: useFormSchema(),
showDefaultActions: false showDefaultActions: false,
}); });
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
@ -49,19 +49,25 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) { if (!isOpen) {
return; return;
} }
// //
const data = modalApi.getData<Demo01ContactApi.Demo01Contact>(); let data = modalApi.getData<Demo01ContactApi.Demo01Contact>();
if (!data || !data.id) { if (!data) {
return; return;
} }
modalApi.lock();
try { if (data.id) {
formData.value = await getDemo01Contact(data.id as number); //
// values modalApi.lock();
await formApi.setValues(formData.value); try {
} finally { data = await getDemo01Contact(data.id);
modalApi.lock(false); } finally {
modalApi.lock(false);
}
} }
// values
formData.value = data;
await formApi.setValues(formData.value);
}, },
}); });
</script> </script>

View File

@ -4,6 +4,7 @@ import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { getDemo02CategoryList } from '#/api/infra/demo/demo02'; import { getDemo02CategoryList } from '#/api/infra/demo/demo02';
import { getRangePickerDefaultProps } from '#/utils/date';
import { handleTree } from '#/utils/tree'; import { handleTree } from '#/utils/tree';
import { useAccess } from '@vben/access'; import { useAccess } from '@vben/access';
@ -82,8 +83,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '创建时间', label: '创建时间',
component: 'RangePicker', component: 'RangePicker',
componentProps: { componentProps: {
...getRangePickerDefaultProps(),
allowClear: true, allowClear: true,
// TODO @puhui999缺了你写的哪个时间选择哈
}, },
}, },
]; ];
@ -120,7 +121,7 @@ export function useGridColumns(
field: 'operation', field: 'operation',
title: '操作', title: '操作',
minWidth: 200, minWidth: 200,
align: 'right', align: 'center',
fixed: 'right', fixed: 'right',
headerAlign: 'center', headerAlign: 'center',
showOverflow: false, showOverflow: false,
@ -133,7 +134,7 @@ export function useGridColumns(
name: 'CellOperation', name: 'CellOperation',
options: [ options: [
{ {
code: 'add_child', // TODO @puhui999append 使用这个单词哈,和之前 vben 官方示例一致 code: 'append',
text: '新增下级', text: '新增下级',
show: hasAccessByCodes(['infra:demo02-category:create']), show: hasAccessByCodes(['infra:demo02-category:create']),
}, },

View File

@ -2,7 +2,6 @@
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02'; import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
import { DocAlert } from '#/components/doc-alert';
import Form from './modules/form.vue'; import Form from './modules/form.vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons'; import { Download, Plus } from '@vben/icons';
@ -12,7 +11,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDemo02Category, exportDemo02Category, getDemo02CategoryList } from '#/api/infra/demo/demo02'; import { deleteDemo02Category, exportDemo02Category, getDemo02CategoryList } from '#/api/infra/demo/demo02';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { downloadByData } from '#/utils/download'; import { downloadByData } from '#/utils/download';
import { ref } from 'vue'; import { h, ref } from 'vue';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@ -50,7 +49,7 @@ function onEdit(row: Demo02CategoryApi.Demo02Category) {
} }
/** 新增下级示例分类 */ /** 新增下级示例分类 */
function onAddChild(row: Demo02CategoryApi.Demo02Category) { function onAppend(row: Demo02CategoryApi.Demo02Category) {
formModalApi.setData({ parentId: row.id }).open(); formModalApi.setData({ parentId: row.id }).open();
} }
@ -76,8 +75,8 @@ async function onDelete(row: Demo02CategoryApi.Demo02Category) {
/** 表格操作按钮的回调函数 */ /** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<Demo02CategoryApi.Demo02Category>) { function onActionClick({ code, row }: OnActionClickParams<Demo02CategoryApi.Demo02Category>) {
switch (code) { switch (code) {
case 'add_child': { case 'append': {
onAddChild(row); onAppend(row);
break; break;
} }
case 'delete': { case 'delete': {
@ -129,8 +128,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<DocAlert title="示例分类" url="https://doc.iocoder.cn/infra/" />
<FormModal @success="onRefresh" /> <FormModal @success="onRefresh" />
<Grid table-title=""> <Grid table-title="">
@ -138,12 +135,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
<Button @click="toggleExpand" class="mr-2"> <Button @click="toggleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }} {{ isExpanded ? '收缩' : '展开' }}
</Button> </Button>
<Button type="primary" @click="onCreate" v-access:code="['infra:demo02-category:create']"> <Button :icon="h(Plus)" type="primary" @click="onCreate" v-access:code="['infra:demo02-category:create']">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['示例分类']) }} {{ $t('ui.actionTitle.create', ['示例分类']) }}
</Button> </Button>
<Button type="primary" class="ml-2" @click="onExport" v-access:code="['infra:demo02-category:export']"> <Button
<Download class="size-5" /> :icon="h(Download)"
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['infra:demo02-category:export']"
>
{{ $t('ui.actionTitle.export') }} {{ $t('ui.actionTitle.export') }}
</Button> </Button>
</template> </template>

View File

@ -25,7 +25,7 @@ const getTitle = computed(() => {
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
layout: 'horizontal', layout: 'horizontal',
schema: useFormSchema(), schema: useFormSchema(),
showDefaultActions: false showDefaultActions: false,
}); });
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
@ -54,36 +54,25 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) { if (!isOpen) {
return; return;
} }
// //
let data = modalApi.getData<Demo02CategoryApi.Demo02Category>(); let data = modalApi.getData<Demo02CategoryApi.Demo02Category>();
if (!data) { if (!data) {
return; return;
} }
//
// TODO @puhui999 dept menu form parentId
if (!data.id && data.parentId) {
parentId.value = data.parentId;
formData.value = { parentId: parentId.value } as Demo02CategoryApi.Demo02Category;
await formApi.setValues(formData.value);
return;
}
if (data.id) { if (data.id) {
// //
modalApi.lock(); modalApi.lock();
try { try {
data = await getDemo02Category(data.id); data = await getDemo02Category(data.id);
formData.value = data;
await formApi.setValues(formData.value);
} finally { } finally {
modalApi.lock(false); modalApi.lock(false);
} }
} else {
//
formData.value = { parentId: 0 } as Demo02CategoryApi.Demo02Category;
await formApi.setValues(formData.value || {});
} }
// values
formData.value = data;
await formApi.setValues(formData.value);
}, },
}); });
</script> </script>

View File

@ -0,0 +1,318 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '名字',
rules: 'required',
component: 'Input',
componentProps: {
placeholder: '请输入名字',
},
},
{
fieldName: 'sex',
label: '性别',
rules: 'required',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
},
{
fieldName: 'birthday',
label: '出生日期',
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
},
},
{
fieldName: 'description',
label: '简介',
rules: 'required',
component: 'Textarea',
componentProps: {
placeholder: '请输入简介',
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '名字',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入名字',
},
},
{
fieldName: 'sex',
label: '性别',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
placeholder: '请选择性别',
},
},
{
fieldName: 'description',
label: '简介',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入简介',
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Student>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
return [
{ type: 'expand', width: 80, slots: { content: 'expand_content' } },
{
field: 'id',
title: '编号',
minWidth: 120,
},
{
field: 'name',
title: '名字',
minWidth: 120,
},
{
field: 'sex',
title: '性别',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
},
},
{
field: 'birthday',
title: '出生日期',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'description',
title: '简介',
minWidth: 120,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 200,
align: 'center',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '学生',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['infra:demo03-student:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:demo03-student:delete']),
},
],
},
},
];
}
// ==================== 子表(学生课程) ====================
/** 新增/修改列表的字段 */
export function useDemo03CourseGridEditColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Course>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
return [
{
field: 'name',
title: '名字',
minWidth: 120,
slots: { default: 'name' },
},
{
field: 'score',
title: '分数',
minWidth: 120,
slots: { default: 'score' },
},
{
field: 'operation',
title: '操作',
minWidth: 60,
align: 'center',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '学生',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'delete',
show: hasAccessByCodes(['infra:demo03-student:delete']),
},
],
},
},
];
}
/** 列表的字段 */
export function useDemo03CourseGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 120,
},
{
field: 'studentId',
title: '学生编号',
minWidth: 120,
},
{
field: 'name',
title: '名字',
minWidth: 120,
},
{
field: 'score',
title: '分数',
minWidth: 120,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 120,
formatter: 'formatDateTime',
},
];
}
// ==================== 子表(学生班级) ====================
/** 新增/修改的表单 */
export function useDemo03GradeFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '名字',
rules: 'required',
component: 'Input',
componentProps: {
placeholder: '请输入名字',
},
},
{
fieldName: 'teacher',
label: '班主任',
rules: 'required',
component: 'Input',
componentProps: {
placeholder: '请输入班主任',
},
},
];
}
/** 列表的字段 */
export function useDemo03GradeGridColumns(): VxeTableGridOptions<Demo03StudentApi.Demo03Grade>['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 120,
},
{
field: 'studentId',
title: '学生编号',
minWidth: 120,
},
{
field: 'name',
title: '名字',
minWidth: 120,
},
{
field: 'teacher',
title: '班主任',
minWidth: 120,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 120,
formatter: 'formatDateTime',
},
];
}

View File

@ -0,0 +1,147 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import Demo03CourseList from './modules/Demo03CourseList.vue';
import Demo03GradeList from './modules/Demo03GradeList.vue';
import Form from './modules/form.vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDemo03Student, exportDemo03Student, getDemo03StudentPage } from '#/api/infra/demo/demo03/inner';
import { $t } from '#/locales';
import { downloadByData } from '#/utils/download';
import { h, ref } from 'vue';
import { useGridColumns, useGridFormSchema } from './data';
/** 子表的列表 */
const subTabsName = ref('demo03Course');
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.reload();
}
/** 导出表格 */
async function onExport() {
const data = await exportDemo03Student(await gridApi.formApi.getValues());
downloadByData(data, '学生.xls');
}
/** 创建学生 */
function onCreate() {
formModalApi.setData({}).open();
}
/** 编辑学生 */
function onEdit(row: Demo03StudentApi.Demo03Student) {
formModalApi.setData(row).open();
}
/** 删除学生 */
async function onDelete(row: Demo03StudentApi.Demo03Student) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo03Student(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<Demo03StudentApi.Demo03Student>) {
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',
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDemo03StudentPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #expand_content="{ row }">
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName">
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
<Demo03CourseList :student-id="row?.id" />
</Tabs.TabPane>
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
<Demo03GradeList :student-id="row?.id" />
</Tabs.TabPane>
</Tabs>
</template>
<template #toolbar-tools>
<Button :icon="h(Plus)" type="primary" @click="onCreate" v-access:code="['infra:demo03-student:create']">
{{ $t('ui.actionTitle.create', ['学生']) }}
</Button>
<Button
:icon="h(Download)"
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['infra:demo03-student:export']"
>
{{ $t('ui.actionTitle.export') }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,107 @@
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import { Plus } from '@vben/icons';
import { Button, Input } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/inner';
import { $t } from '#/locales';
import { h, nextTick, watch } from 'vue';
import { useDemo03CourseGridEditColumns } from '../data';
const props = defineProps<{
studentId?: any; //
}>();
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<Demo03StudentApi.Demo03Course>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
}
}
const [Demo03CourseGrid, demo03CourseGridApi] = useVbenVxeGrid({
gridOptions: {
columns: useDemo03CourseGridEditColumns(onActionClick),
border: true,
showOverflow: true,
autoResize: true,
keepSource: true,
rowConfig: {
keyField: 'id',
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 删除学生课程 */
const onDelete = async (row: Demo03StudentApi.Demo03Course) => {
await demo03CourseGridApi.grid.remove(row);
};
/** 添加学生课程 */
const handleAdd = async () => {
await demo03CourseGridApi.grid.insertAt({} as Demo03StudentApi.Demo03Course, -1);
};
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): Demo03StudentApi.Demo03Course[] => {
//
const allData = demo03CourseGridApi.grid.getData();
const removedData = demo03CourseGridApi.grid.getRemoveRecords();
const removedIds = new Set(removedData.map((row) => row.id));
//
const currentData = allData.filter((row) => !removedIds.has(row.id));
// id
const insertedData = demo03CourseGridApi.grid.getInsertRecords().map((row) => {
delete row.id;
return row;
});
return [...currentData, ...insertedData];
},
});
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
if (!val) {
return;
}
await nextTick();
await demo03CourseGridApi.grid.loadData(await getDemo03CourseListByStudentId(props.studentId!));
},
);
</script>
<template>
<Demo03CourseGrid class="mx-4">
<template #name="{ row }">
<Input v-model:value="row.name" />
</template>
<template #score="{ row }">
<Input v-model:value="row.score" />
</template>
</Demo03CourseGrid>
<div class="flex justify-center">
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['infra:demo03-student:create']">
{{ $t('ui.actionTitle.create', ['学生课程']) }}
</Button>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/inner';
import { nextTick, watch } from 'vue';
import { useDemo03CourseGridColumns } from '../data';
const props = defineProps<{
studentId?: any; //
}>();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useDemo03CourseGridColumns(),
height: 'auto',
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
});
/** 刷新表格 */
const onRefresh = async () => {
await gridApi.grid.loadData(await getDemo03CourseListByStudentId(props.studentId!));
};
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
if (!val) {
return;
}
await nextTick();
await onRefresh();
},
{ immediate: true },
);
</script>
<template>
<div class="mx-4">
<Grid table-title="" />
</div>
</template>

View File

@ -0,0 +1,43 @@
<script lang="ts" setup>
import { useVbenForm } from '#/adapter/form';
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/inner';
import { nextTick, watch } from 'vue';
import { useDemo03GradeFormSchema } from '../data';
const props = defineProps<{
studentId?: any; //
}>();
const [Demo03GradeForm, demo03GradeFormApi] = useVbenForm({
layout: 'horizontal',
schema: useDemo03GradeFormSchema(),
showDefaultActions: false,
});
/** 暴露出表单校验方法和表单值获取方法 */
defineExpose({
validate: async () => {
const { valid } = await demo03GradeFormApi.validate();
return valid;
},
getValues: demo03GradeFormApi.getValues,
});
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
if (!val) {
return;
}
await nextTick();
await demo03GradeFormApi.setValues(await getDemo03GradeByStudentId(props.studentId!));
},
);
</script>
<template>
<Demo03GradeForm class="mx-4" />
</template>

View File

@ -0,0 +1,56 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/inner';
import { nextTick, watch } from 'vue';
import { useDemo03GradeGridColumns } from '../data';
const props = defineProps<{
studentId?: any; //
}>();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useDemo03GradeGridColumns(),
height: 'auto',
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
});
/** 刷新表格 */
const onRefresh = async () => {
await gridApi.grid.loadData([await getDemo03GradeByStudentId(props.studentId!)]);
};
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
if (!val) {
return;
}
await nextTick();
await onRefresh();
},
{ immediate: true },
);
</script>
<template>
<div class="mx-4">
<Grid table-title="" />
</div>
</template>

View File

@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import { useVbenModal } from '@vben/common-ui';
import { message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDemo03Student, getDemo03Student, updateDemo03Student } from '#/api/infra/demo/demo03/inner';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useFormSchema } from '../data';
import Demo03CourseForm from './Demo03CourseForm.vue';
import Demo03GradeForm from './Demo03GradeForm.vue';
const emit = defineEmits(['success']);
const formData = ref<Demo03StudentApi.Demo03Student>();
const getTitle = computed(() => {
return formData.value?.id ? $t('ui.actionTitle.edit', ['学生']) : $t('ui.actionTitle.create', ['学生']);
});
/** 子表的表单 */
const subTabsName = ref('demo03Course');
const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
//
const demo03GradeValid = await demo03GradeFormRef.value?.validate();
if (!demo03GradeValid) {
subTabsName.value = 'demo03Grade';
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
//
data.demo03Courses = demo03CourseFormRef.value?.getData();
data.demo03Grade = await demo03GradeFormRef.value?.getValues();
try {
await (formData.value?.id ? updateDemo03Student(data) : createDemo03Student(data));
//
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
if (!data) {
return;
}
if (data.id) {
//
modalApi.lock();
try {
data = await getDemo03Student(data.id);
} finally {
modalApi.lock(false);
}
}
// values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName">
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
<Demo03CourseForm ref="demo03CourseFormRef" :student-id="formData?.id" />
</Tabs.TabPane>
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
<Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData?.id" />
</Tabs.TabPane>
</Tabs>
</Modal>
</template>

View File

@ -0,0 +1,252 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '名字',
rules: 'required',
component: 'Input',
componentProps: {
placeholder: '请输入名字',
},
},
{
fieldName: 'sex',
label: '性别',
rules: 'required',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
},
{
fieldName: 'birthday',
label: '出生日期',
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
},
},
{
fieldName: 'description',
label: '简介',
rules: 'required',
component: 'Textarea',
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '名字',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入名字',
},
},
{
fieldName: 'sex',
label: '性别',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
placeholder: '请选择性别',
},
},
{
fieldName: 'description',
label: '简介',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入简介',
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Student>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Student>['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 120,
},
{
field: 'name',
title: '名字',
minWidth: 120,
},
{
field: 'sex',
title: '性别',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
},
},
{
field: 'birthday',
title: '出生日期',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'description',
title: '简介',
minWidth: 120,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
minWidth: 200,
align: 'center',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '学生',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'edit',
show: hasAccessByCodes(['infra:demo03-student:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['infra:demo03-student:delete']),
},
],
},
},
];
}
// ==================== 子表(学生课程) ====================
/** 新增/修改的列表的字段 */
export function useDemo03CourseGridEditColumns(
onActionClick?: OnActionClickFn<Demo03StudentApi.Demo03Course>,
): VxeTableGridOptions<Demo03StudentApi.Demo03Course>['columns'] {
return [
{
field: 'name',
title: '名字',
minWidth: 120,
slots: { default: 'name' },
},
{
field: 'score',
title: '分数',
minWidth: 120,
slots: { default: 'score' },
},
{
field: 'operation',
title: '操作',
minWidth: 60,
align: 'center',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: 'id',
nameTitle: '学生',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'delete',
show: hasAccessByCodes(['infra:demo03-student:delete']),
},
],
},
},
];
}
// ==================== 子表(学生班级) ====================
/** 新增/修改的表单 */
export function useDemo03GradeFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '名字',
rules: 'required',
component: 'Input',
componentProps: {
placeholder: '请输入名字',
},
},
{
fieldName: 'teacher',
label: '班主任',
rules: 'required',
component: 'Input',
componentProps: {
placeholder: '请输入班主任',
},
},
];
}

View File

@ -0,0 +1,131 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import Form from './modules/form.vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download, Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteDemo03Student, exportDemo03Student, getDemo03StudentPage } from '#/api/infra/demo/demo03/normal';
import { $t } from '#/locales';
import { downloadByData } from '#/utils/download';
import { h } from 'vue';
import { useGridColumns, useGridFormSchema } from './data';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function onExport() {
const data = await exportDemo03Student(await gridApi.formApi.getValues());
downloadByData(data, '学生.xls');
}
/** 创建学生 */
function onCreate() {
formModalApi.setData({}).open();
}
/** 编辑学生 */
function onEdit(row: Demo03StudentApi.Demo03Student) {
formModalApi.setData(row).open();
}
/** 删除学生 */
async function onDelete(row: Demo03StudentApi.Demo03Student) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteDemo03Student(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<Demo03StudentApi.Demo03Student>) {
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',
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDemo03StudentPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<Demo03StudentApi.Demo03Student>,
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<Button :icon="h(Plus)" type="primary" @click="onCreate" v-access:code="['infra:demo03-student:create']">
{{ $t('ui.actionTitle.create', ['学生']) }}
</Button>
<Button
:icon="h(Download)"
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['infra:demo03-student:export']"
>
{{ $t('ui.actionTitle.export') }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,108 @@
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import { Plus } from '@vben/icons';
import { Button, Input } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/normal';
import { $t } from '#/locales';
import { h, nextTick, watch } from 'vue';
import { useDemo03CourseGridEditColumns } from '../data';
const props = defineProps<{
studentId?: any; //
}>();
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<Demo03StudentApi.Demo03Course>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
}
}
const [Demo03CourseGrid, demo03CourseGridApi] = useVbenVxeGrid({
gridOptions: {
columns: useDemo03CourseGridEditColumns(onActionClick),
border: true,
showOverflow: true,
autoResize: true,
keepSource: true,
rowConfig: {
keyField: 'id',
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 删除学生课程 */
const onDelete = async (row: Demo03StudentApi.Demo03Course) => {
await demo03CourseGridApi.grid.remove(row);
};
/** 添加学生课程 */
const handleAdd = async () => {
await demo03CourseGridApi.grid.insertAt({} as Demo03StudentApi.Demo03Course, -1);
};
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): Demo03StudentApi.Demo03Course[] => {
//
const allData = demo03CourseGridApi.grid.getData();
const removedData = demo03CourseGridApi.grid.getRemoveRecords();
const removedIds = new Set(removedData.map((row) => row.id));
//
const currentData = allData.filter((row) => !removedIds.has(row.id));
// id
const insertedData = demo03CourseGridApi.grid.getInsertRecords().map((row) => {
delete row.id;
return row;
});
return [...currentData, ...insertedData];
},
});
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
if (!val) {
return;
}
await nextTick();
await demo03CourseGridApi.grid.loadData(await getDemo03CourseListByStudentId(props.studentId!));
},
{ immediate: true },
);
</script>
<template>
<Demo03CourseGrid class="mx-4">
<template #name="{ row }">
<Input v-model:value="row.name" />
</template>
<template #score="{ row }">
<Input v-model:value="row.score" />
</template>
</Demo03CourseGrid>
<div class="flex justify-center">
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['infra:demo03-student:create']">
{{ $t('ui.actionTitle.create', ['学生课程']) }}
</Button>
</div>
</template>

View File

@ -0,0 +1,43 @@
<script lang="ts" setup>
import { useVbenForm } from '#/adapter/form';
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';
import { watch } from 'vue';
import { useDemo03GradeFormSchema } from '../data';
const props = defineProps<{
studentId?: any; //
}>();
const [Demo03GradeForm, demo03GradeFormApi] = useVbenForm({
layout: 'horizontal',
schema: useDemo03GradeFormSchema(),
showDefaultActions: false,
});
/** 暴露出表单校验方法和表单值获取方法 */
defineExpose({
validate: async () => {
const { valid } = await demo03GradeFormApi.validate();
return valid;
},
getValues: demo03GradeFormApi.getValues,
});
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
if (!val) {
return;
}
await demo03GradeFormApi.setValues(await getDemo03GradeByStudentId(props.studentId!));
},
{ immediate: true },
);
</script>
<template>
<Demo03GradeForm class="mx-4" />
</template>

View File

@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import { useVbenModal } from '@vben/common-ui';
import { message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDemo03Student, getDemo03Student, updateDemo03Student } from '#/api/infra/demo/demo03/normal';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useFormSchema } from '../data';
import Demo03CourseForm from './Demo03CourseForm.vue';
import Demo03GradeForm from './Demo03GradeForm.vue';
const emit = defineEmits(['success']);
const formData = ref<Demo03StudentApi.Demo03Student>();
const getTitle = computed(() => {
return formData.value?.id ? $t('ui.actionTitle.edit', ['学生']) : $t('ui.actionTitle.create', ['学生']);
});
/** 子表的表单 */
const subTabsName = ref('demo03Course');
const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
//
const demo03GradeValid = await demo03GradeFormRef.value?.validate();
if (!demo03GradeValid) {
subTabsName.value = 'demo03Grade';
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
//
data.demo03Courses = demo03CourseFormRef.value?.getData();
data.demo03Grade = await demo03GradeFormRef.value?.getValues();
try {
await (formData.value?.id ? updateDemo03Student(data) : createDemo03Student(data));
//
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
if (!data) {
return;
}
if (data.id) {
//
modalApi.lock();
try {
data = await getDemo03Student(data.id);
} finally {
modalApi.lock(false);
}
}
// values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<!-- 子表的表单 -->
<Tabs v-model:active-key="subTabsName">
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
<Demo03CourseForm ref="demo03CourseFormRef" :student-id="formData?.id" />
</Tabs.TabPane>
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
<Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData?.id" />
</Tabs.TabPane>
</Tabs>
</Modal>
</template>

View File

@ -1,12 +1,14 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemMailTemplateApi } from '#/api/system/mail/template'; import type { SystemMailTemplateApi } from '#/api/system/mail/template';
import type { ComputedRef } from 'vue';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getSimpleMailAccountList } from '#/api/system/mail/account'; import { getSimpleMailAccountList } from '#/api/system/mail/account';
import { CommonStatusEnum } from '#/utils/constants'; import { CommonStatusEnum } from '#/utils/constants';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { getRangePickerDefaultProps } from '#/utils/date'; import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { useAccess } from '@vben/access'; import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
@ -59,7 +61,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'Input', component: 'Input',
componentProps: { componentProps: {
placeholder: '请输入发送人名称', placeholder: '请输入发送人名称',
} },
}, },
{ {
fieldName: 'title', fieldName: 'title',
@ -97,7 +99,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'Textarea', component: 'Textarea',
componentProps: { componentProps: {
placeholder: '请输入备注', placeholder: '请输入备注',
} },
}, },
]; ];
} }
@ -130,7 +132,7 @@ export function useSendMailFormSchema(): VbenFormSchema[] {
placeholder: '请输入收件邮箱', placeholder: '请输入收件邮箱',
}, },
rules: z.string().email('请输入正确的邮箱地址'), rules: z.string().email('请输入正确的邮箱地址'),
} },
]; ];
} }
@ -154,7 +156,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
allowClear: true, allowClear: true,
placeholder: '请输入模板编码', placeholder: '请输入模板编码',
} },
}, },
{ {
fieldName: 'name', fieldName: 'name',
@ -163,7 +165,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
allowClear: true, allowClear: true,
placeholder: '请输入模板名称', placeholder: '请输入模板名称',
} },
}, },
{ {
fieldName: 'accountId', fieldName: 'accountId',
@ -192,6 +194,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns<T = SystemMailTemplateApi.SystemMailTemplate>( export function useGridColumns<T = SystemMailTemplateApi.SystemMailTemplate>(
onActionClick: OnActionClickFn<T>, onActionClick: OnActionClickFn<T>,
getAccountName: ComputedRef<(cellValue: number) => string>,
): VxeTableGridOptions['columns'] { ): VxeTableGridOptions['columns'] {
return [ return [
{ {
@ -214,11 +217,11 @@ export function useGridColumns<T = SystemMailTemplateApi.SystemMailTemplate>(
title: '模板标题', title: '模板标题',
minWidth: 120, minWidth: 120,
}, },
// TODO @puhui999这里差一个翻译
{ {
field: 'accountId', field: 'accountId',
title: '邮箱账号', title: '邮箱账号',
minWidth: 120, minWidth: 120,
formatter: ({ cellValue }) => getAccountName.value(cellValue),
}, },
{ {
field: 'nickname', field: 'nickname',

View File

@ -1,17 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table'; 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 type { SystemMailTemplateApi } from '#/api/system/mail/template';
import { DocAlert } from '#/components/doc-alert';
import Form from './modules/form.vue';
import SendForm from './modules/send-form.vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons'; import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import Form from './modules/form.vue';
import SendForm from './modules/send-form.vue';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleMailAccountList } from '#/api/system/mail/account';
import { deleteMailTemplate, getMailTemplatePage } from '#/api/system/mail/template'; import { deleteMailTemplate, getMailTemplatePage } from '#/api/system/mail/template';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@ -65,32 +68,32 @@ async function onDelete(row: SystemMailTemplateApi.SystemMailTemplate) {
} }
/** 表格操作按钮的回调函数 */ /** 表格操作按钮的回调函数 */
function onActionClick({ function onActionClick({ code, row }: OnActionClickParams<SystemMailTemplateApi.SystemMailTemplate>) {
code,
row,
}: OnActionClickParams<SystemMailTemplateApi.SystemMailTemplate>) {
switch (code) { switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': { case 'delete': {
onDelete(row); onDelete(row);
break; break;
} }
case 'edit': {
onEdit(row);
break;
}
case 'send': { case 'send': {
onSend(row); onSend(row);
break; break;
} }
} }
} }
const mailAccountList = ref<SystemMailAccountApi.SystemMailAccount[]>([]);
const getAccountName = computed(
() => (cellValue: number) => mailAccountList.value.find((item) => item.id === cellValue)?.mail || '',
);
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(onActionClick), columns: useGridColumns(onActionClick, getAccountName),
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
@ -113,6 +116,18 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
} as VxeTableGridOptions<SystemMailTemplateApi.SystemMailTemplate>, } as VxeTableGridOptions<SystemMailTemplateApi.SystemMailTemplate>,
}); });
/** 获取邮箱账号精简列表 */
async function initMailAccountList() {
try {
mailAccountList.value = await getSimpleMailAccountList();
} catch (error) {
console.error('获取邮箱账号精简列表失败', error);
}
}
/** 初始化 */
initMailAccountList();
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height>

View File

@ -2,8 +2,9 @@ import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemNotifyMessageApi } from '#/api/system/notify/message'; import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { getRangePickerDefaultProps } from '#/utils/date'; import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { useAccess } from '@vben/access'; import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess(); const { hasAccessByCodes } = useAccess();
@ -44,10 +45,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '模版类型', label: '模版类型',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions( options: getDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE, 'number'),
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
'number',
),
allowClear: true, allowClear: true,
placeholder: '请选择模版类型', placeholder: '请选择模版类型',
}, },
@ -103,11 +101,17 @@ export function useGridColumns<T = SystemNotifyMessageApi.SystemNotifyMessage>(
title: '模版内容', title: '模版内容',
minWidth: 200, minWidth: 200,
}, },
// TODO @puhui999这个参数展示不对
{ {
field: 'templateParams', field: 'templateParams',
title: '模版参数', title: '模版参数',
minWidth: 180, minWidth: 180,
formatter: ({ cellValue }) => {
try {
return JSON.stringify(cellValue);
} catch {
return '';
}
},
}, },
{ {
field: 'templateType', field: 'templateType',