!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',
name: 'InfraCodegenEdit',
component: () => import('#/views/infra/codegen/edit.vue'),
component: () => import('#/views/infra/codegen/edit/index.vue'),
meta: {
title: '修改生成配置',
activeMenu: '/infra/codegen',

View File

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

View File

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

View File

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

View File

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

View File

@ -1,56 +1,44 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { createCodegenList, getSchemaTableList } from '#/api/infra/codegen';
import { getDataSourceConfigList } from '#/api/infra/data-source-config';
import { reactive, ref, unref } from 'vue';
import { reactive } from 'vue';
import { $t } from '@vben/locales';
import { useImportTableFormSchema } from '#/views/infra/codegen/data';
import { useImportTableColumns, useImportTableFormSchema } from '#/views/infra/codegen/data';
/** 定义组件事件 */
const emit = defineEmits<{
(e: 'success'): void;
}>();
const dataSourceConfigList = ref<InfraDataSourceConfigApi.InfraDataSourceConfig[]>([]);
const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
dataSourceConfigId: undefined,
dataSourceConfigId: 0,
tableNames: [], //
});
/** 表格实例 */
const [Grid, gridApi] = useVbenVxeGrid({
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useImportTableFormSchema([]),
schema: useImportTableFormSchema(),
submitOnChange: true,
},
gridOptions: {
// TODO @puhui999
columns: [
{ type: 'checkbox', width: 40 },
{ field: 'name', title: '表名称', minWidth: 200 },
{ field: 'comment', title: '表描述', minWidth: 200 },
],
columns: useImportTableColumns(),
height: 600,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
// TODO @puhui999使 formValues.dataSourceConfigId
if (formValues.dataSourceConfigId === undefined) {
if (unref(dataSourceConfigList).length > 0) {
formValues.dataSourceConfigId = unref(dataSourceConfigList)[0]?.id;
} else {
return [];
}
}
formData.dataSourceConfigId = formValues.dataSourceConfigId;
return await getSchemaTableList({
pageNo: page.currentPage,
@ -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>
<template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ const getTitle = computed(() => {
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
@ -54,36 +54,25 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) {
return;
}
//
let data = modalApi.getData<Demo02CategoryApi.Demo02Category>();
if (!data) {
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) {
//
modalApi.lock();
try {
data = await getDemo02Category(data.id);
formData.value = data;
await formApi.setValues(formData.value);
} finally {
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>

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

View File

@ -1,17 +1,20 @@
<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 { 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 { Plus } from '@vben/icons';
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 { getSimpleMailAccountList } from '#/api/system/mail/account';
import { deleteMailTemplate, getMailTemplatePage } from '#/api/system/mail/template';
import { $t } from '#/locales';
import { computed, ref } from 'vue';
import { useGridColumns, useGridFormSchema } from './data';
@ -65,32 +68,32 @@ async function onDelete(row: SystemMailTemplateApi.SystemMailTemplate) {
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<SystemMailTemplateApi.SystemMailTemplate>) {
function onActionClick({ code, row }: OnActionClickParams<SystemMailTemplateApi.SystemMailTemplate>) {
switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
case 'send': {
onSend(row);
break;
}
}
}
const mailAccountList = ref<SystemMailAccountApi.SystemMailAccount[]>([]);
const getAccountName = computed(
() => (cellValue: number) => mailAccountList.value.find((item) => item.id === cellValue)?.mail || '',
);
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
columns: useGridColumns(onActionClick, getAccountName),
height: 'auto',
keepSource: true,
proxyConfig: {
@ -113,6 +116,18 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
} as VxeTableGridOptions<SystemMailTemplateApi.SystemMailTemplate>,
});
/** 获取邮箱账号精简列表 */
async function initMailAccountList() {
try {
mailAccountList.value = await getSimpleMailAccountList();
} catch (error) {
console.error('获取邮箱账号精简列表失败', error);
}
}
/** 初始化 */
initMailAccountList();
</script>
<template>
<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 { SystemNotifyMessageApi } from '#/api/system/notify/message';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess();
@ -44,10 +45,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '模版类型',
component: 'Select',
componentProps: {
options: getDictOptions(
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
'number',
),
options: getDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE, 'number'),
allowClear: true,
placeholder: '请选择模版类型',
},
@ -103,11 +101,17 @@ export function useGridColumns<T = SystemNotifyMessageApi.SystemNotifyMessage>(
title: '模版内容',
minWidth: 200,
},
// TODO @puhui999这个参数展示不对
{
field: 'templateParams',
title: '模版参数',
minWidth: 180,
formatter: ({ cellValue }) => {
try {
return JSON.stringify(cellValue);
} catch {
return '';
}
},
},
{
field: 'templateType',