fix(infra): 修复前端代码生成模板的组件绑定与主子表行为

修复 Vben5 字典控件、日期及树选择属性、导入文件移除和 API 导入路径。
修复主子表数据获取、临时编号、必填校验和主表切换后的旧数据残留。
补齐工具方法与组件导入,修正 Ele 工具栏标签及 Vue3 单选多选绑定。
同步 Vue2 组件标签与 prop 声明、旧 Vben 日期范围格式。
更新生成快照,增加跨前端组件契约测试。

验证:85 条 CodegenEngine 测试、138 份 Vue 模板编译通过;
Codex、Qwen、DeepSeek 三路评审通过。
This commit is contained in:
YunaiV
2026-10-06 02:09:32 +08:00
parent c8a584c51e
commit 3ed98e152a
120 changed files with 3131 additions and 1753 deletions
@@ -244,9 +244,13 @@
Editor,
#end
},
props:[
'${subJoinColumn.javaField}'
],// ${subJoinColumn.columnComment}(主表的关联字段)
props: {
// ${subJoinColumn.columnComment}(主表的关联字段)
${subJoinColumn.javaField}: {
type: #if ($subJoinColumn.javaType == "String")String#else Number#end,
default: undefined
}
},
data() {
return {
// 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -78,7 +78,7 @@
</el-table>
#if ($table.templateType == 11)
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
<Pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<${subSimpleClassName}Form ref="formRef" @success="getList" />
@@ -98,9 +98,13 @@
${subSimpleClassName}Form
},
#end
props:[
'${subJoinColumn.javaField}'
],// ${subJoinColumn.columnComment}(主表的关联字段)
props: {
// ${subJoinColumn.columnComment}(主表的关联字段)
${subJoinColumn.javaField}: {
type: #if ($subJoinColumn.javaType == "String")String#else Number#end,
default: undefined
}
},
data() {
return {
// 遮罩层
@@ -86,7 +86,7 @@
</el-button>
</el-col>
#end
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<RightToolbar :showSearch.sync="showSearch" @queryTable="getList"></RightToolbar>
</el-row>
#if ($importEnable)
<input ref="importFileRef" type="file" style="display: none" accept=".xls,.xlsx" @change="handleImportFileChange" />
@@ -186,7 +186,7 @@
## 特殊:树表专属逻辑(树不需要分页)
#if ( $table.templateType != 2 )
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
<Pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
#end
<!-- 对话框(添加 / 修改) -->
@@ -79,7 +79,7 @@
<el-radio
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@@ -111,7 +111,7 @@
<el-radio
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@@ -233,7 +233,7 @@
<el-radio
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@@ -90,7 +90,7 @@
<el-radio
v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
@@ -78,6 +78,12 @@ export const searchFormSchema: FormSchema[] = [
},
#elseif($column.htmlType == "datetime")
component: 'RangePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
allowClear: true,
},
#end
colProps: { span: 8 },
},
@@ -112,7 +112,10 @@ const [Modal, modalApi] = useVbenModal({
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany) ## 一对多
## TODO 列表值校验?
if (!${subClassNameVar}FormRef.value?.validate()) {
subTabsName.value = '${subClassNameVar}';
return;
}
#else
try {
await ${subClassNameVar}FormRef.value?.validate()
@@ -48,6 +48,11 @@ function beforeUpload(file: FileType) {
return false;
}
/** 移除文件 */
function handleRemove() {
fileRef.value = null;
}
/** 下载导入模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
@@ -58,7 +63,12 @@ async function handleDownload() {
<template>
<Modal title="导入${table.classComment}" class="w-1/3">
<div class="mx-4">
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
<Upload
:max-count="1"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
:on-remove="handleRemove"
>
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
</div>
@@ -65,12 +65,7 @@ const queryParams = reactive({
#end
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.listOperationCondition != 'BETWEEN')
$column.javaField: undefined,
#end
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
$column.javaField: undefined,
#end
$column.javaField: undefined,
#end
#end
})
@@ -197,7 +192,17 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
const data = await export${simpleClassName}(params);
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
}finally {
exportLoading.value = false;
@@ -46,25 +46,54 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
];
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): ${simpleClassName}Api.${subSimpleClassName}[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
#set ($hasRequiredColumn = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($hasRequiredColumn = true)
#end
#end
/** 校验子表必填字段 */
function validate(): boolean {
#if ($hasRequiredColumn)
for (const [index, row] of getData().entries()) {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
if (
row.${column.javaField} === null ||
row.${column.javaField} === undefined ||
(typeof row.${column.javaField} === 'string' && String(row.${column.javaField}).trim() === '') ||
(Array.isArray(row.${column.javaField}) && row.${column.javaField}.length === 0)
) {
message.error(`第 #[[${index + 1}]]# 行:${column.columnComment}不能为空`);
return false;
}
#end
#end
}
#end
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -103,6 +132,17 @@ defineExpose({
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
formData.value = {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
$column.javaField: [],
#else
$column.javaField: undefined,
#end
#end
#end
};
if (!val) {
return;
}
@@ -120,6 +160,7 @@ watch(
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#set ($comment = $column.columnComment)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
@@ -36,7 +36,7 @@ export function useFormSchema(): VbenFormSchema[] {
id: 0,
${treeNameColumn.javaField}: '顶级${table.classComment}',
});
return handleTree(data);
return handleTree(data, 'id', '${treeParentColumn.javaField}');
},
labelField: '${treeNameColumn.javaField}',
valueField: 'id',
@@ -89,7 +89,7 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -306,7 +306,7 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -446,6 +446,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#if ($column.createOperation || $column.updateOperation)
#if (!$column.primaryKey && $column.listOperationResult && $column.id != $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
@@ -540,7 +541,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -590,7 +591,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#end
#if ($table.templateType == 12) ## 内嵌情况
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.listOperationResult)
@@ -6,7 +6,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message#if ($table.templateType == 11), Tabs#end } from 'ant-design-vue';
import { message#if ($table.templateType == 10 || $table.templateType == 12), Tabs#end } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
@@ -69,15 +69,11 @@ const [Modal, modalApi] = useVbenModal({
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany) ## 一对多
## TODO 列表值校验?
#else
const ${subClassNameVar}Valid = await ${subClassNameVar}FormRef.value?.validate();
if (!${subClassNameVar}Valid) {
subTabsName.value = '${subClassNameVar}';
return;
}
#end
#end
#end
#end
@@ -51,6 +51,11 @@ function beforeUpload(file: FileType) {
return false;
}
/** 移除文件 */
function handleRemove() {
formApi.setFieldValue('file', undefined);
}
/** 下载模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
@@ -67,6 +72,7 @@ async function handleDownload() {
:max-count="1"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
:on-remove="handleRemove"
>
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
@@ -14,7 +14,7 @@
#if ($subTable.subJoinMany) ## 一对多
import { Plus } from "@vben/icons";
import { Button, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'ant-design-vue';
import { Button, Checkbox, CheckboxGroup, DatePicker, Input, message, RadioGroup, Select, Tabs, Textarea } from 'ant-design-vue';
import { ImageUpload, FileUpload } from "#/components/upload";
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { use${subSimpleClassName}GridEditColumns } from '../data';
@@ -59,26 +59,54 @@ const handleDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): ${apiName}.${subSimpleClassName}[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
#set ($hasRequiredColumn = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($hasRequiredColumn = true)
#end
#end
/** 校验子表必填字段 */
function validate(): boolean {
#if ($hasRequiredColumn)
for (const [index, row] of getData().entries()) {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
if (
row.${column.javaField} === null ||
row.${column.javaField} === undefined ||
(typeof row.${column.javaField} === 'string' && String(row.${column.javaField}).trim() === '') ||
(Array.isArray(row.${column.javaField}) && row.${column.javaField}.length === 0)
) {
message.error(`第 #[[${index + 1}]]# 行:${column.columnComment}不能为空`);
return false;
}
#end
#end
}
#end
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
},
{ immediate: true },
@@ -110,10 +138,11 @@ defineExpose({
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
},
{ immediate: true },
@@ -118,7 +118,10 @@ const [Modal, modalApi] = useVbenModal({
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany) ## 一对多
## TODO 列表值校验?
if (!${subClassNameVar}FormRef.value?.validate()) {
subTabsName.value = '${subClassNameVar}';
return;
}
#else
try {
await ${subClassNameVar}FormRef.value?.validate()
@@ -46,6 +46,11 @@ function beforeUpload(file: File) {
return false;
}
/** 移除文件 */
function handleRemove() {
fileRef.value = null;
}
/** 下载导入模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
@@ -56,7 +61,8 @@ async function handleDownload() {
<template>
<Modal title="导入${table.classComment}" class="w-1/3">
<div class="mx-4">
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload"
:on-remove="handleRemove">
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
</div>
@@ -77,12 +77,7 @@ const queryParams = reactive({
#end
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.listOperationCondition != 'BETWEEN')
$column.javaField: undefined,
#end
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
$column.javaField: undefined,
#end
$column.javaField: undefined,
#end
#end
})
@@ -209,7 +204,17 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
const data = await export${simpleClassName}(params);
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
}finally {
exportLoading.value = false;
@@ -18,7 +18,7 @@ import { computed, ref, h, onMounted, watch, nextTick } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button, Checkbox, CheckboxGroup, DatePicker#if (!$subTable.subJoinMany), Form, FormItem#end, Input, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end, TextArea } from 'antdv-next';
import { Button, Checkbox, CheckboxGroup, DatePicker#if (!$subTable.subJoinMany), Form, FormItem#end, Input#if ($subTable.subJoinMany), message#end, Radio, RadioGroup, Select#if ($selectOptionEnable), SelectOption#end, TextArea } from 'antdv-next';
import { $t } from '#/locales';
#if ($subTable.subJoinMany) ## 一对多
@@ -52,25 +52,54 @@ async function handleDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
];
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): ${simpleClassName}Api.${subSimpleClassName}[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
#set ($hasRequiredColumn = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($hasRequiredColumn = true)
#end
#end
/** 校验子表必填字段 */
function validate(): boolean {
#if ($hasRequiredColumn)
for (const [index, row] of getData().entries()) {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
if (
row.${column.javaField} === null ||
row.${column.javaField} === undefined ||
(typeof row.${column.javaField} === 'string' && String(row.${column.javaField}).trim() === '') ||
(Array.isArray(row.${column.javaField}) && row.${column.javaField}.length === 0)
) {
message.error(`第 #[[${index + 1}]]# 行:${column.columnComment}不能为空`);
return false;
}
#end
#end
}
#end
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -109,6 +138,17 @@ defineExpose({
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
formData.value = {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
$column.javaField: [],
#else
$column.javaField: undefined,
#end
#end
#end
};
if (!val) {
return;
}
@@ -126,6 +166,7 @@ watch(
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#set ($comment = $column.columnComment)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
@@ -36,7 +36,7 @@ export function useFormSchema(): VbenFormSchema[] {
id: 0,
${treeNameColumn.javaField}: '顶级${table.classComment}',
});
return handleTree(data);
return handleTree(data, 'id', '${treeParentColumn.javaField}');
},
labelField: '${treeNameColumn.javaField}',
valueField: 'id',
@@ -89,7 +89,7 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -305,7 +305,7 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -444,6 +444,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#if ($column.createOperation || $column.updateOperation)
#if (!$column.primaryKey && $column.listOperationResult && $column.id != $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
@@ -538,7 +539,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -587,7 +588,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#end
#if ($table.templateType == 12) ## 内嵌情况
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.listOperationResult)
@@ -69,15 +69,11 @@ const [Modal, modalApi] = useVbenModal({
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany) ## 一对多
## TODO 列表值校验?
#else
const ${subClassNameVar}Valid = await ${subClassNameVar}FormRef.value?.validate();
if (!${subClassNameVar}Valid) {
subTabsName.value = '${subClassNameVar}';
return;
}
#end
#end
#end
#end
@@ -49,6 +49,11 @@ function beforeUpload(file: File) {
return false;
}
/** 移除文件 */
function handleRemove() {
formApi.setFieldValue('file', undefined);
}
/** 下载模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
@@ -65,6 +70,7 @@ async function handleDownload() {
:max-count="1"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
:on-remove="handleRemove"
>
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
@@ -26,6 +26,7 @@ import {
CheckboxGroup,
DatePicker,
Input,
message,
RadioGroup,
Select,
#if ($selectOptionEnable)
@@ -77,26 +78,54 @@ const handleDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): ${apiName}.${subSimpleClassName}[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
#set ($hasRequiredColumn = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($hasRequiredColumn = true)
#end
#end
/** 校验子表必填字段 */
function validate(): boolean {
#if ($hasRequiredColumn)
for (const [index, row] of getData().entries()) {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
if (
row.${column.javaField} === null ||
row.${column.javaField} === undefined ||
(typeof row.${column.javaField} === 'string' && String(row.${column.javaField}).trim() === '') ||
(Array.isArray(row.${column.javaField}) && row.${column.javaField}.length === 0)
) {
message.error(`第 #[[${index + 1}]]# 行:${column.columnComment}不能为空`);
return false;
}
#end
#end
}
#end
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
},
{ immediate: true },
@@ -128,10 +157,11 @@ defineExpose({
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
},
{ immediate: true },
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -9,7 +9,7 @@ import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
#if($table.templateType == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${table.businessName}';
import { handleTree } from '@vben/utils'
#end
## 特殊:主子表专属逻辑
@@ -23,7 +23,7 @@ import { handleTree } from '@vben/utils'
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
const emit = defineEmits(['success']);
@@ -111,7 +111,10 @@ const [Modal, modalApi] = useVbenModal({
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany) ## 一对多
## TODO 列表值校验?
if (!${subClassNameVar}FormRef.value?.validate()) {
subTabsName.value = '${subClassNameVar}';
return;
}
#else
try {
await ${subClassNameVar}FormRef.value?.validate()
@@ -180,7 +183,7 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
@@ -201,8 +204,8 @@ const [Modal, modalApi] = useVbenModal({
#set ($dictMethod = "boolean")
#end
#if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
<el-form-item label="${comment}" prop="${javaField}">
<el-tree-select
<ElFormItem label="${comment}" prop="${javaField}">
<ElTreeSelect
v-model="formData.${javaField}"
:data="${classNameVar}Tree"
#if ($treeNameColumn.javaField == "name")
@@ -222,100 +225,100 @@ const [Modal, modalApi] = useVbenModal({
default-expand-all
placeholder="请选择${comment}"
/>
</el-form-item>
</ElFormItem>
#elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<el-form-item label="${comment}" prop="${javaField}">
<el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
</el-form-item>
<ElFormItem label="${comment}" prop="${javaField}">
<ElInput v-model="formData.${javaField}" placeholder="请输入${comment}" />
</ElFormItem>
#elseif($column.htmlType == "imageUpload")## 图片上传
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ImageUpload v-model="formData.${javaField}" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "fileUpload")## 文件上传
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<FileUpload v-model="formData.${javaField}" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "editor")## 文本编辑器
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<RichTextarea v-model="formData.${javaField}" height="500px" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "select")## 下拉框
<el-form-item label="${comment}" prop="${javaField}">
<el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElSelect v-model="formData.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
#else##没数据字典
<el-option label="请选择字典生成" value="" />
<ElOption label="请选择字典生成" value="" />
#end
</el-select>
</el-form-item>
</ElSelect>
</ElFormItem>
#elseif($column.htmlType == "checkbox")## 多选框
<el-form-item label="${comment}" prop="${javaField}">
<el-checkbox-group v-model="formData.${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElCheckboxGroup v-model="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-checkbox
<ElCheckbox
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-checkbox>
</ElCheckbox>
#else##没数据字典
<el-checkbox label="请选择字典生成" />
<ElCheckbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
</ElCheckboxGroup>
</ElFormItem>
#elseif($column.htmlType == "radio")## 单选框
<el-form-item label="${comment}" prop="${javaField}">
<el-radio-group v-model="formData.${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElRadioGroup v-model="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-radio
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</ElRadio>
#else##没数据字典
<el-radio :label="1">请选择字典生成</el-radio>
<ElRadio :label="1">请选择字典生成</ElRadio>
#end
</el-radio-group>
</el-form-item>
</ElRadioGroup>
</ElFormItem>
#elseif($column.htmlType == "datetime")## 时间框
<el-form-item label="${comment}" prop="${javaField}">
<el-date-picker
<ElFormItem label="${comment}" prop="${javaField}">
<ElDatePicker
v-model="formData.${javaField}"
value-format="x"
placeholder="选择${comment}"
/>
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "textarea")## 文本框
<el-form-item label="${comment}" prop="${javaField}">
<el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
</el-form-item>
<ElFormItem label="${comment}" prop="${javaField}">
<ElInput v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
</ElFormItem>
#end
#end
#end
</el-form>
</ElForm>
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<ElTabs v-model="subTabsName">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
#end
</Modal>
</template>
@@ -47,6 +47,16 @@ function handleChange(file: any) {
}
}
/** 移除文件 */
function handleRemove() {
fileRef.value = null;
}
/** 超出文件数量 */
function handleExceed() {
ElMessage.warning('只能选择一个文件,请先移除已选文件');
}
/** 下载导入模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
@@ -61,6 +71,8 @@ async function handleDownload() {
:limit="1"
accept=".xls,.xlsx"
:on-change="handleChange"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:auto-upload="false"
>
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -26,10 +26,10 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
#if (${table.templateType} == 2)## 树表接口
import { handleTree,isEmpty } from '@vben/utils'
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#else## 标准表接口
import { isEmpty } from '@vben/utils';
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
#if ($importEnable)
import ${simpleClassName}Import from './modules/import-form.vue';
@@ -65,12 +65,7 @@ const queryParams = reactive({
#end
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.listOperationCondition != 'BETWEEN')
$column.javaField: undefined,
#end
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
$column.javaField: undefined,
#end
$column.javaField: undefined,
#end
#end
})
@@ -195,7 +190,17 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await export${simpleClassName}(queryParams);
const params = cloneDeep(queryParams) as any;
#foreach ($column in $columns)
#if ($column.listOperation)
#if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
if (params.${column.javaField} && Array.isArray(params.${column.javaField})) {
params.${column.javaField} = (params.${column.javaField} as string[]).join(',');
}
#end
#end
#end
const data = await export${simpleClassName}(params);
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
}finally {
exportLoading.value = false;
@@ -228,7 +233,7 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
@@ -247,49 +252,49 @@ onMounted(() => {
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input")
<el-form-item label="${comment}">
<el-input
<ElFormItem label="${comment}">
<ElInput
v-model="queryParams.${javaField}"
placeholder="请输入${comment}"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
</ElFormItem>
#elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox")
<el-form-item label="${comment}">
<el-select
<ElFormItem label="${comment}">
<ElSelect
v-model="queryParams.${javaField}"
placeholder="请选择${comment}"
clearable
class="!w-[240px]"
>
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
#else## 未设置 dictType 数据字典的情况
<el-option label="请选择字典生成" value="" />
<ElOption label="请选择字典生成" value="" />
#end
</el-select>
</el-form-item>
</ElSelect>
</ElFormItem>
#elseif($column.htmlType == "datetime")
#if ($column.listOperationCondition != "BETWEEN")## 非范围
<el-form-item label="${comment}">
<el-date-picker
<ElFormItem label="${comment}">
<ElDatePicker
v-model="queryParams.${javaField}"
value-format="YYYY-MM-DD"
placeholder="选择${comment}"
clearable
class="!w-[240px]"
/>
</el-form-item>
</ElFormItem>
#else## 范围
<el-form-item label="${comment}">
<el-date-picker
<ElFormItem label="${comment}">
<ElDatePicker
v-model="queryParams.${javaField}"
type="daterange"
value-format="YYYY-MM-DD"
@@ -298,33 +303,33 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
</ElFormItem>
#end
#end
#end
#end
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="${table.classComment}">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
#if (${table.templateType} == 2)
<el-button @click="handleExpand" class="mr-2">
<ElButton @click="handleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</el-button>
</ElButton>
#end
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -332,18 +337,18 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:create']"
>
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
</el-button>
</ElButton>
#if ($importEnable)
<el-button
<ElButton
class="ml-2"
type="primary"
@click="handleImport"
v-access:code="['${permissionPrefix}:import']"
>
导入
</el-button>
</ElButton>
#end
<el-button
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -352,9 +357,9 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
</ElButton>
#if ($table.templateType != 2 && $deleteBatchEnable)
<el-button
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -363,11 +368,11 @@ onMounted(() => {
v-access:code="['${table.moduleName}:${simpleClassName_strikeCase}:delete']"
>
批量删除
</el-button>
</ElButton>
#end
</TableToolbar>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
#if ( $table.templateType == 2 )
@@ -395,27 +400,27 @@ onMounted(() => {
#end
>
#if ($table.templateType != 2 && $deleteBatchEnable)
<vxe-column type="checkbox" width="40"></vxe-column>
<VxeColumn type="checkbox" width="40"></VxeColumn>
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 12 && $subTables && $subTables.size() > 0 )
<!-- 子表的列表 -->
<vxe-column type="expand" width="60">
<VxeColumn type="expand" width="60">
<template #content="{ row }">
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName" class="mx-8">
<ElTabs v-model="subTabsName" class="mx-8">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}List :${subJoinColumn_strikeCase}="row?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
</template>
</vxe-column>
</VxeColumn>
#end
#foreach($column in $columns)
#if ($column.listOperationResult)
@@ -424,28 +429,28 @@ onMounted(() => {
#set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set ($comment=$column.columnComment)
#if ($column.javaType == "LocalDateTime")## 时间类型
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
{{formatDateTime(row.${javaField})}}
</template>
</vxe-column>
</VxeColumn>
#elseif($column.dictType && "" != $column.dictType)## 数据字典
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="row.${javaField}" />
</template>
</vxe-column>
</VxeColumn>
#elseif ($table.templateType == 2 && $javaField == $treeNameColumn.javaField)
<vxe-column field="${javaField}" title="${comment}" align="center" tree-node/>
<VxeColumn field="${javaField}" title="${comment}" align="center" tree-node/>
#else
<vxe-column field="${javaField}" title="${comment}" align="center" />
<VxeColumn field="${javaField}" title="${comment}" align="center" />
#end
#end
#end
<vxe-column field="operation" title="操作" align="center">
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
#if ( $table.templateType == 2 )
<el-button
<ElButton
size="small"
type="primary"
link
@@ -453,9 +458,9 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:create']"
>
新增下级
</el-button>
</ElButton>
#end
<el-button
<ElButton
size="small"
type="primary"
link
@@ -463,8 +468,8 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -476,14 +481,14 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
#if ( $table.templateType != 2 )
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -498,17 +503,17 @@ onMounted(() => {
#if ($table.templateType == 11) ## erp情况
<ContentWrap>
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<ElTabs v-model="subTabsName">
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
<el-tab-pane name="$subClassNameVar" label="${subTable.classComment}">
<ElTabPane name="$subClassNameVar" label="${subTable.classComment}">
<${subSimpleClassName}List :${subJoinColumn_strikeCase}="select${simpleClassName}?.id" />
</el-tab-pane>
</ElTabPane>
#end
</el-tabs>
</ElTabs>
</ContentWrap>
#end
</Page>
@@ -3,7 +3,7 @@
#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { FormRules } from 'element-plus';
import { computed, ref, reactive } from 'vue';
@@ -17,7 +17,7 @@
import { $t } from '#/locales';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${subSimpleClassName}, create${subSimpleClassName}, update${subSimpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
const emit = defineEmits(['success']);
const getTitle = computed(() => {
@@ -107,7 +107,7 @@
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
@@ -128,83 +128,83 @@
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<el-form-item label="${comment}" prop="${javaField}">
<el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
</el-form-item>
<ElFormItem label="${comment}" prop="${javaField}">
<ElInput v-model="formData.${javaField}" placeholder="请输入${comment}" />
</ElFormItem>
#elseif($column.htmlType == "imageUpload")## 图片上传
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ImageUpload v-model="formData.${javaField}" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "fileUpload")## 文件上传
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<FileUpload v-model="formData.${javaField}" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "editor")## 文本编辑器
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<RichTextarea v-model="formData.${javaField}" height="500px" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "select")## 下拉框
<el-form-item label="${comment}" prop="${javaField}">
<el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElSelect v-model="formData.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
#else##没数据字典
<el-option label="请选择字典生成" value="" />
<ElOption label="请选择字典生成" value="" />
#end
</el-select>
</el-form-item>
</ElSelect>
</ElFormItem>
#elseif($column.htmlType == "checkbox")## 多选框
<el-form-item label="${comment}" prop="${javaField}">
<el-checkbox-group v-model="formData.${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElCheckboxGroup v-model="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-checkbox
<ElCheckbox
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-checkbox>
</ElCheckbox>
#else##没数据字典
<el-checkbox label="请选择字典生成" />
<ElCheckbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
</ElCheckboxGroup>
</ElFormItem>
#elseif($column.htmlType == "radio")## 单选框
<el-form-item label="${comment}" prop="${javaField}">
<el-radio-group v-model="formData.${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElRadioGroup v-model="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-radio
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</ElRadio>
#else##没数据字典
<el-radio :label="1">请选择字典生成</el-radio>
<ElRadio :label="1">请选择字典生成</ElRadio>
#end
</el-radio-group>
</el-form-item>
</ElRadioGroup>
</ElFormItem>
#elseif($column.htmlType == "datetime")## 时间框
<el-form-item label="${comment}" prop="${javaField}">
<el-date-picker
<ElFormItem label="${comment}" prop="${javaField}">
<ElDatePicker
v-model="formData.${javaField}"
value-format="x"
placeholder="选择${comment}"
/>
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "textarea")## 文本框
<el-form-item label="${comment}" prop="${javaField}">
<el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
</el-form-item>
<ElFormItem label="${comment}" prop="${javaField}">
<ElInput v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
</ElFormItem>
#end
#end
#end
</el-form>
</ElForm>
</Modal>
</template>
@@ -5,26 +5,61 @@
#set ($subClassNameVar = $subClassNameVars.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#if ($subTable.subJoinMany)
import type { VxeTableInstance } from '#/adapter/vxe-table';
#else
import type { FormRules } from 'element-plus';
#end
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
#if ($subTable.subJoinMany)
import { ref, watch } from 'vue';
#else
import { nextTick, reactive, ref, watch } from 'vue';
#end
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
#if ($subTable.subJoinMany)
import { Plus } from '@vben/icons';
#end
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { $t } from '#/locales';
#set ($hasCheckbox = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && $column.id != $subJoinColumn.id && $column.htmlType == "checkbox")
#set ($hasCheckbox = true)
#end
#end
import {
#if ($subTable.subJoinMany)
ElButton,
#else
ElForm,
ElFormItem,
#end
#if ($hasCheckbox)
ElCheckbox,
ElCheckboxGroup,
#end
ElDatePicker,
ElInput,
#if ($subTable.subJoinMany)
ElMessage,
#end
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from 'element-plus';
#if ($subTable.subJoinMany) ## 一对多
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { Plus } from "@vben/icons";
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
#else
import type { FormRules } from 'element-plus';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
import { $t } from '#/locales';
const props = defineProps<{
${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段)
@@ -43,24 +78,54 @@ async function onDelete(row: ${simpleClassName}Api.${subSimpleClassName}) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${simpleClassName}Api.${subSimpleClassName}[] => {
const data = list.value as ${simpleClassName}Api.${subSimpleClassName}[];
const removeRecords = tableRef.value?.getRemoveRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
const insertRecords = tableRef.value?.getInsertRecords() as ${simpleClassName}Api.${subSimpleClassName}[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): ${simpleClassName}Api.${subSimpleClassName}[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
#set ($hasRequiredColumn = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($hasRequiredColumn = true)
#end
#end
/** 校验子表必填字段 */
function validate(): boolean {
#if ($hasRequiredColumn)
for (const [index, row] of getData().entries()) {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
if (
row.${column.javaField} === null ||
row.${column.javaField} === undefined ||
(typeof row.${column.javaField} === 'string' && String(row.${column.javaField}).trim() === '') ||
(Array.isArray(row.${column.javaField}) && row.${column.javaField}.length === 0)
) {
ElMessage.error(`第 #[[${index + 1}]]# 行:${column.columnComment}不能为空`);
return false;
}
#end
#end
}
#end
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -99,6 +164,17 @@ defineExpose({
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
formData.value = {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if ($column.htmlType == "checkbox")
$column.javaField: [],
#else
$column.javaField: undefined,
#end
#end
#end
};
if (!val) {
return;
}
@@ -112,10 +188,11 @@ watch(
<template>
#if ($subTable.subJoinMany) ## 一对多
<vxe-table ref="tableRef" :data="list" show-overflow class="mx-4">
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
#foreach($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#set ($comment = $column.columnComment)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($javaType = $column.javaType)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
@@ -127,99 +204,99 @@ watch(
#end
#if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<el-input v-model="row.${javaField}" />
<ElInput v-model="row.${javaField}" />
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "imageUpload")## 图片上传
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<ImageUpload v-model="row.${javaField}" />
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "fileUpload")## 文件上传
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<FileUpload v-model="row.${javaField}" />
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "select")## 下拉框
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<el-select v-model="row.${javaField}" placeholder="请选择${comment}">
<ElSelect v-model="row.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
#else##没数据字典
<el-option label="请选择字典生成" value="" />
<ElOption label="请选择字典生成" value="" />
#end
</el-select>
</ElSelect>
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "checkbox")## 多选框
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<el-checkbox-group v-model="row.${javaField}">
<ElCheckboxGroup v-model="row.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-checkbox
<ElCheckbox
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-checkbox>
</ElCheckbox>
#else##没数据字典
<el-checkbox label="请选择字典生成" />
<ElCheckbox label="请选择字典生成" />
#end
</el-checkbox-group>
</ElCheckboxGroup>
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "radio")## 单选框
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<el-radio-group v-model="row.${javaField}">
<ElRadioGroup v-model="row.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-radio
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</ElRadio>
#else##没数据字典
<el-radio :label="1">请选择字典生成</el-radio>
<ElRadio :label="1">请选择字典生成</ElRadio>
#end
</el-radio-group>
</ElRadioGroup>
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "datetime")## 时间框
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<el-date-picker
<ElDatePicker
v-model="row.${javaField}"
type="datetime"
value-format="x"
format="YYYY-MM-DD HH:mm:ss"
/>
</template>
</vxe-column>
</VxeColumn>
#elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<el-input v-model="row.${javaField}" type="textarea" />
<ElInput v-model="row.${javaField}" type="textarea" />
</template>
</vxe-column>
</VxeColumn>
#end
#end
#end
<vxe-column field="operation" title="操作" align="center">
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="danger"
link
@@ -227,17 +304,17 @@ watch(
v-access:code="['${permissionPrefix}:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<div class="flex justify-center mt-4">
<el-button :icon="h(Plus)" type="primary" plain @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
<ElButton :icon="Plus" type="primary" plain @click="handleAdd" v-access:code="['${permissionPrefix}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</el-button>
</ElButton>
</div>
#else
<el-form
<ElForm
ref="formRef"
class="mx-4"
:model="formData"
@@ -259,83 +336,83 @@ watch(
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<el-form-item label="${comment}" prop="${javaField}">
<el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
</el-form-item>
<ElFormItem label="${comment}" prop="${javaField}">
<ElInput v-model="formData.${javaField}" placeholder="请输入${comment}" />
</ElFormItem>
#elseif($column.htmlType == "imageUpload")## 图片上传
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ImageUpload v-model="formData.${javaField}" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "fileUpload")## 文件上传
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<FileUpload v-model="formData.${javaField}" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "editor")## 文本编辑器
<el-form-item label="${comment}" prop="${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<RichTextarea v-model="formData.${javaField}" height="500px" />
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "select")## 下拉框
<el-form-item label="${comment}" prop="${javaField}">
<el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElSelect v-model="formData.${javaField}" placeholder="请选择${comment}">
#if ("" != $dictType)## 有数据字典
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
#else##没数据字典
<el-option label="请选择字典生成" value="" />
<ElOption label="请选择字典生成" value="" />
#end
</el-select>
</el-form-item>
</ElSelect>
</ElFormItem>
#elseif($column.htmlType == "checkbox")## 多选框
<el-form-item label="${comment}" prop="${javaField}">
<el-checkbox-group v-model="formData.${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElCheckboxGroup v-model="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-checkbox
<ElCheckbox
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-checkbox>
</ElCheckbox>
#else##没数据字典
<el-checkbox label="请选择字典生成" />
<ElCheckbox label="请选择字典生成" />
#end
</el-checkbox-group>
</el-form-item>
</ElCheckboxGroup>
</ElFormItem>
#elseif($column.htmlType == "radio")## 单选框
<el-form-item label="${comment}" prop="${javaField}">
<el-radio-group v-model="formData.${javaField}">
<ElFormItem label="${comment}" prop="${javaField}">
<ElRadioGroup v-model="formData.${javaField}">
#if ("" != $dictType)## 有数据字典
<el-radio
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</ElRadio>
#else##没数据字典
<el-radio :label="1">请选择字典生成</el-radio>
<ElRadio :label="1">请选择字典生成</ElRadio>
#end
</el-radio-group>
</el-form-item>
</ElRadioGroup>
</ElFormItem>
#elseif($column.htmlType == "datetime")## 时间框
<el-form-item label="${comment}" prop="${javaField}">
<el-date-picker
<ElFormItem label="${comment}" prop="${javaField}">
<ElDatePicker
v-model="formData.${javaField}"
value-format="x"
placeholder="选择${comment}"
/>
</el-form-item>
</ElFormItem>
#elseif($column.htmlType == "textarea")## 文本框
<el-form-item label="${comment}" prop="${javaField}">
<el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
</el-form-item>
<ElFormItem label="${comment}" prop="${javaField}">
<ElInput v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
</ElFormItem>
#end
#end
#end
</el-form>
</ElForm>
#end
</template>
@@ -6,13 +6,14 @@
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($subIndex))
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${table.businessName}';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
@@ -31,13 +32,13 @@
#end
#if ($table.templateType == 11) ## erp
import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { isEmpty } from '@vben/utils';
import { delete${subSimpleClassName},#if ($deleteBatchEnable) delete${subSimpleClassName}List,#end get${subSimpleClassName}Page } from '#/api/${table.moduleName}/${table.businessName}';
import { cloneDeep, isEmpty } from '@vben/utils';
#else
#if ($subTable.subJoinMany) ## 一对多
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
#else
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { get${subSimpleClassName}By${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
#end
#end
@@ -203,7 +204,7 @@ onMounted(() => {
<div class="h-[600px]">
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
@@ -222,49 +223,49 @@ onMounted(() => {
#set ($dictMethod = "boolean")
#end
#if ($column.htmlType == "input")
<el-form-item label="${comment}">
<el-input
<ElFormItem label="${comment}">
<ElInput
v-model="queryParams.${javaField}"
placeholder="请输入${comment}"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
</ElFormItem>
#elseif ($column.htmlType == "select" || $column.htmlType == "radio" || $column.htmlType == "checkbox")
<el-form-item label="${comment}">
<el-select
<ElFormItem label="${comment}">
<ElSelect
v-model="queryParams.${javaField}"
placeholder="请选择${comment}"
clearable
class="!w-[240px]"
>
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
#else## 未设置 dictType 数据字典的情况
<el-option label="请选择字典生成" value="" />
<ElOption label="请选择字典生成" value="" />
#end
</el-select>
</el-form-item>
</ElSelect>
</ElFormItem>
#elseif($column.htmlType == "datetime")
#if ($column.listOperationCondition != "BETWEEN")## 非范围
<el-form-item label="${comment}">
<el-date-picker
<ElFormItem label="${comment}">
<ElDatePicker
v-model="queryParams.${javaField}"
value-format="YYYY-MM-DD"
placeholder="选择${comment}"
clearable
class="!w-[240px]"
/>
</el-form-item>
</ElFormItem>
#else## 范围
<el-form-item label="${comment}">
<el-date-picker
<ElFormItem label="${comment}">
<ElDatePicker
v-model="queryParams.${javaField}"
type="daterange"
value-format="YYYY-MM-DD"
@@ -273,18 +274,18 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
</ElFormItem>
#end
#end
#end
#end
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
@@ -294,7 +295,7 @@ onMounted(() => {
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -302,9 +303,9 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:create']"
>
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
</el-button>
</ElButton>
#if ($deleteBatchEnable)
<el-button
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -313,11 +314,11 @@ onMounted(() => {
v-access:code="['${table.moduleName}:${simpleClassName_strikeCase}:delete']"
>
批量删除
</el-button>
</ElButton>
#end
</TableToolbar>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -328,7 +329,7 @@ onMounted(() => {
#end
>
#if ($deleteBatchEnable)
<vxe-column type="checkbox" width="40"></vxe-column>
<VxeColumn type="checkbox" width="40"></VxeColumn>
#end
#foreach($column in $subColumns)
#if ($column.listOperationResult)
@@ -336,27 +337,27 @@ onMounted(() => {
#set ($javaField = $column.javaField)
#set ($comment=$column.columnComment)
#if ($column.javaType == "LocalDateTime")## 时间类型
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
{{formatDateTime(row.${javaField})}}
</template>
</vxe-column>
</VxeColumn>
#elseif($column.dictType && "" != $column.dictType)## 数据字典
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="row.${javaField}" />
</template>
</vxe-column>
</VxeColumn>
#elseif ($table.templateType == 2 && $javaField == $treeNameColumn.javaField)
<vxe-column field="${javaField}" title="${comment}" align="center" tree-node/>
<VxeColumn field="${javaField}" title="${comment}" align="center" tree-node/>
#else
<vxe-column field="${javaField}" title="${comment}" align="center" />
<VxeColumn field="${javaField}" title="${comment}" align="center" />
#end
#end
#end
<vxe-column field="operation" title="操作" align="center">
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -364,8 +365,8 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -374,13 +375,13 @@ onMounted(() => {
v-access:code="['${permissionPrefix}:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -394,7 +395,7 @@ onMounted(() => {
</div>
#else
<ContentWrap title="${subTable.classComment}列表">
<vxe-table
<VxeTable
:data="list"
show-overflow
:loading="loading"
@@ -405,23 +406,23 @@ onMounted(() => {
#set ($javaField = $column.javaField)
#set ($comment=$column.columnComment)
#if ($column.javaType == "LocalDateTime")## 时间类型
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
{{formatDateTime(row.${javaField})}}
</template>
</vxe-column>
</VxeColumn>
#elseif($column.dictType && "" != $column.dictType)## 数据字典
<vxe-column field="${javaField}" title="${comment}" align="center">
<VxeColumn field="${javaField}" title="${comment}" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="row.${javaField}" />
</template>
</vxe-column>
</VxeColumn>
#else
<vxe-column field="${javaField}" title="${comment}" align="center" />
<VxeColumn field="${javaField}" title="${comment}" align="center" />
#end
#end
#end
</vxe-table>
</VxeTable>
</ContentWrap>
#end
</template>
@@ -36,13 +36,14 @@ export function useFormSchema(): VbenFormSchema[] {
id: 0,
${treeNameColumn.javaField}: '顶级${table.classComment}',
});
return handleTree(data);
return handleTree(data, 'id', '${treeParentColumn.javaField}');
},
labelField: '${treeNameColumn.javaField}',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择上级${table.classComment}',
treeDefaultExpandAll: true,
defaultExpandAll: true,
checkStrictly: true,
},
rules: 'selectRequired',
},
@@ -89,7 +90,7 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -105,13 +106,12 @@ export function useFormSchema(): VbenFormSchema[] {
#else##没数据字典
options: [],
#end
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
#elseif($column.htmlType == "datetime")## 时间框
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -308,7 +308,7 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -324,13 +324,12 @@ export function use${subSimpleClassName}FormSchema(): VbenFormSchema[] {
#else##没数据字典
options: [],
#end
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
#elseif($column.htmlType == "datetime")## 时间框
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -450,6 +449,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#if ($column.createOperation || $column.updateOperation)
#if (!$column.primaryKey && $column.listOperationResult && $column.id != $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
@@ -544,7 +544,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
placeholder: '请选择${comment}',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
component: 'CheckboxGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
@@ -560,13 +560,12 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#else##没数据字典
options: [],
#end
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
#elseif($column.htmlType == "datetime")## 时间框
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -596,7 +595,7 @@ export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${api
#end
#if ($table.templateType == 12) ## 内嵌情况
/** 列表的字段 */
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${simpleClassName}Api.${subSimpleClassName}>['columns'] {
export function use${subSimpleClassName}GridColumns(): VxeTableGridOptions<${apiName}.${subSimpleClassName}>['columns'] {
return [
#foreach($column in $subColumns)
#if ($column.listOperationResult)
@@ -6,7 +6,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage#if ($table.templateType == 11), ElTabs, ElTabPane#end } from 'element-plus';
import { ElMessage#if ($table.templateType == 10 || $table.templateType == 12), ElTabPane, ElTabs#end } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
@@ -69,15 +69,11 @@ const [Modal, modalApi] = useVbenModal({
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subClassNameVar = $subClassNameVars.get($index))
#if ($subTable.subJoinMany) ## 一对多
## TODO 列表值校验?
#else
const ${subClassNameVar}Valid = await ${subClassNameVar}FormRef.value?.validate();
if (!${subClassNameVar}Valid) {
subTabsName.value = '${subClassNameVar}';
return;
}
#end
#end
#end
#end
@@ -50,6 +50,16 @@ function handleChange(file: any) {
}
}
/** 移除文件 */
function handleRemove() {
formApi.setFieldValue('file', undefined);
}
/** 超出文件数量 */
function handleExceed() {
ElMessage.warning('只能选择一个文件,请先移除已选文件');
}
/** 下载模板 */
async function handleDownload() {
const data = await import${simpleClassName}Template();
@@ -66,6 +76,8 @@ async function handleDownload() {
:limit="1"
accept=".xls,.xlsx"
:on-change="handleChange"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:auto-upload="false"
>
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
@@ -14,10 +14,9 @@
#if ($subTable.subJoinMany) ## 一对多
import { Plus } from "@vben/icons";
import { ElButton, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElOption, ElRadioGroup, ElCheckboxGroup, ElDatePicker } from 'element-plus';
import { ElButton, ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElMessage, ElOption, ElRadio, ElRadioGroup, ElSelect, ElTabPane, ElTabs } from 'element-plus';
import { ImageUpload, FileUpload } from "#/components/upload";
import type { OnActionClickParams } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { use${subSimpleClassName}GridEditColumns } from '../data';
import { get${subSimpleClassName}ListBy${SubJoinColumnName} } from '#/api/${table.moduleName}/${table.businessName}';
#else
@@ -31,22 +30,9 @@ const props = defineProps<{
}>()
#if ($subTable.subJoinMany) ## 一对多
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<${apiName}.${subSimpleClassName}>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: use${subSimpleClassName}GridEditColumns(onActionClick),
columns: use${subSimpleClassName}GridEditColumns(),
border: true,
showOverflow: true,
autoResize: true,
@@ -65,7 +51,7 @@ gridOptions: {
/** 添加${subTable.classComment} */
const onAdd = async () => {
await gridApi.grid.insertAt({} as ${simpleClassName}Api.${subSimpleClassName}, -1);
await gridApi.grid.insertAt({} as ${apiName}.${subSimpleClassName}, -1);
}
/** 删除${subTable.classComment} */
@@ -73,26 +59,54 @@ const onDelete = async (row: ${apiName}.${subSimpleClassName}) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): ${apiName}.${subSimpleClassName}[] => {
const data = gridApi.grid.getData() as ${apiName}.${subSimpleClassName}[];
const removeRecords = gridApi.grid.getRemoveRecords() as ${apiName}.${subSimpleClassName}[];
const insertRecords = gridApi.grid.getInsertRecords() as ${apiName}.${subSimpleClassName}[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): ${apiName}.${subSimpleClassName}[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
#set ($hasRequiredColumn = false)
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
#set ($hasRequiredColumn = true)
#end
#end
/** 校验子表必填字段 */
function validate(): boolean {
#if ($hasRequiredColumn)
for (const [index, row] of getData().entries()) {
#foreach ($column in $subColumns)
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey && $column.id != $subJoinColumn.id)
if (
row.${column.javaField} === null ||
row.${column.javaField} === undefined ||
(typeof row.${column.javaField} === 'string' && String(row.${column.javaField}).trim() === '') ||
(Array.isArray(row.${column.javaField}) && row.${column.javaField}.length === 0)
) {
ElMessage.error(`第 #[[${index + 1}]]# 行:${column.columnComment}不能为空`);
return false;
}
#end
#end
}
#end
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
},
{ immediate: true },
@@ -124,10 +138,11 @@ defineExpose({
watch(
() => props.${subJoinColumn.javaField},
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField}!));
},
{ immediate: true },
@@ -144,7 +159,7 @@ watch(
#if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
#elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
<template #${javaField}="{ row }">
<el-input v-model="row.${javaField}" />
<ElInput v-model="row.${javaField}" />
</template>
#elseif($column.htmlType == "imageUpload")## 图片上传
<template #${javaField}="{ row }">
@@ -156,29 +171,29 @@ watch(
</template>
#elseif($column.htmlType == "select")## 下拉框
<template #${javaField}="{ row, column }">
<el-select v-model="row.${javaField}" class="w-full">
<el-option v-for="option in column.params.options" :key="option.value" :value="option.value" :label="option.label" />
</el-select>
<ElSelect v-model="row.${javaField}" class="w-full">
<ElOption v-for="option in column.params.options" :key="option.value" :value="option.value" :label="option.label" />
</ElSelect>
</template>
#elseif($column.htmlType == "checkbox")## 多选框
<template #${javaField}="{ row, column }">
<el-checkbox-group v-model="row.${javaField}">
<el-checkbox v-for="option in column.params.options" :key="option.value" :label="option.value">
<ElCheckboxGroup v-model="row.${javaField}">
<ElCheckbox v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
</ElCheckbox>
</ElCheckboxGroup>
</template>
#elseif($column.htmlType == "radio")## 单选框
<template #${javaField}="{ row, column }">
<el-radio-group v-model="row.${javaField}">
<el-radio v-for="option in column.params.options" :key="option.value" :label="option.value">
<ElRadioGroup v-model="row.${javaField}">
<ElRadio v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</el-radio>
</el-radio-group>
</ElRadio>
</ElRadioGroup>
</template>
#elseif($column.htmlType == "datetime")## 时间框
<template #${javaField}="{ row }">
<el-date-picker
<ElDatePicker
v-model="row.${javaField}"
type="datetime"
value-format="x"
@@ -187,16 +202,29 @@ watch(
</template>
#elseif($column.htmlType == "textarea" || $column.htmlType == "editor")## 文本框
<template #${javaField}="{ row }">
<el-input v-model="row.${javaField}" type="textarea" />
<ElInput v-model="row.${javaField}" type="textarea" />
</template>
#end
#end
#end
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'danger', link: true,
icon: ACTION_ICON.DELETE,
auth: ['${permissionPrefix}:delete'],
onClick: onDelete.bind(null, row),
},
]"
/>
</template>
</Grid>
<div class="flex justify-center -mt-4">
<el-button :icon="Plus" type="primary" plain @click="onAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
<ElButton :icon="Plus" type="primary" plain @click="onAdd" v-access:code="['${subTable.moduleName}:${simpleClassName_strikeCase}:create']">
{{ $t('ui.actionTitle.create', ['${subTable.classComment}']) }}
</el-button>
</ElButton>
</div>
#else
<Form class="mx-4" />
@@ -0,0 +1,234 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link CodegenEngine} 的前端组件契约回归测试
*
* @author 芋道源码
*/
public class CodegenEngineFrontendContractTest extends CodegenEngineAbstractTest {
@ParameterizedTest
@EnumSource(value = CodegenFrontTypeEnum.class, names = {
"VUE3_VBEN5_ANTD_SCHEMA", "VUE3_VBEN5_ANTDV_NEXT_SCHEMA", "VUE3_VBEN5_EP_SCHEMA"})
public void testExecute_dictionaryControls(CodegenFrontTypeEnum frontType) {
// 准备参数:同一字典的多选、单选和数字必填字段
CodegenTableDO table = getTable("student").setFrontType(frontType.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
columns.add(new CodegenColumnDO().setJavaField("tags").setJavaType("String")
.setColumnComment("标签").setDictType("system_user_sex").setHtmlType("checkbox")
.setCreateOperation(true).setUpdateOperation(true).setNullable(false).setPrimaryKey(false));
columns.add(new CodegenColumnDO().setJavaField("status").setJavaType("Integer")
.setColumnComment("状态").setDictType("system_user_sex").setHtmlType("radio")
.setCreateOperation(true).setUpdateOperation(true).setNullable(false).setPrimaryKey(false));
columns.add(new CodegenColumnDO().setJavaField("amount").setJavaType("Integer")
.setColumnComment("金额").setHtmlType("inputNumber").setDictType("")
.setCreateOperation(true).setUpdateOperation(true).setNullable(false).setPrimaryKey(false));
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 调试用:导出真实生成结果,供前端组件验证
String outputPath = System.getProperty("codegen.contractOutput");
if (outputPath != null) {
writeResult(result, outputPath + "/" + frontType.name());
}
// 断言:组选项不能变成 boolean Checkbox,必填数字不能被 Zod 推导为 null
String data = findFile(result, "/data.ts");
assertTrue(data.contains("component: 'CheckboxGroup'"));
assertFalse(data.contains("component: 'Checkbox',"));
assertTrue(data.contains("rules: 'required'"));
assertFalse(data.contains("z.number("));
if (frontType == CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA) {
assertTrue(data.contains("isButton: true"));
assertFalse(data.contains("buttonStyle:"));
assertFalse(data.contains("optionType:"));
assertFalse(data.contains("allowClear:"));
} else {
assertTrue(data.contains("optionType: 'button'"));
assertFalse(data.contains("clearable:"));
}
}
@Test
public void testExecute_eleTreeParentSelectable() {
// 准备参数
CodegenTableDO table = getTable("category").setFrontType(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType())
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("category"), null, null);
// 断言:父节点可作为上级,使用 Element Plus 属性
String data = findFile(result, "/data.ts");
assertTrue(data.contains("defaultExpandAll: true"));
assertTrue(data.contains("checkStrictly: true"));
assertFalse(data.contains("treeDefaultExpandAll"));
}
@ParameterizedTest
@EnumSource(value = CodegenFrontTypeEnum.class, names = {
"VUE3_VBEN5_ANTD_SCHEMA", "VUE3_VBEN5_ANTD_GENERAL",
"VUE3_VBEN5_ANTDV_NEXT_SCHEMA", "VUE3_VBEN5_ANTDV_NEXT_GENERAL",
"VUE3_VBEN5_EP_SCHEMA", "VUE3_VBEN5_EP_GENERAL"})
public void testExecute_importRemovalClearsPayload(CodegenFrontTypeEnum frontType) {
// 准备参数
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
CodegenTableDO table = getTable("student").setFrontType(frontType.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
// 断言:移除可视文件时也清空提交来源
String form = findFile(result, "/import-form.vue");
assertTrue(form.contains(":on-remove=\"handleRemove\""));
assertTrue(form.contains("formApi.setFieldValue('file', undefined)") || form.contains("fileRef.value = null"));
if (frontType == CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA
|| frontType == CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL) {
assertTrue(form.contains(":on-exceed=\"handleExceed\""));
}
}
@ParameterizedTest
@EnumSource(value = CodegenFrontTypeEnum.class, names = {
"VUE3_VBEN5_ANTD_SCHEMA", "VUE3_VBEN5_ANTD_GENERAL",
"VUE3_VBEN5_ANTDV_NEXT_SCHEMA", "VUE3_VBEN5_ANTDV_NEXT_GENERAL",
"VUE3_VBEN5_EP_SCHEMA", "VUE3_VBEN5_EP_GENERAL"})
public void testExecute_masterChildEditing(CodegenFrontTypeEnum frontType) {
// 准备参数:一对多联系人和一对一班主任
CodegenTableDO table = getTable("student").setFrontType(frontType.getType())
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType());
CodegenTableDO contact = getTable("contact").setFrontType(frontType.getType())
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
CodegenTableDO teacher = getTable("teacher").setFrontType(frontType.getType())
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(200L).setSubJoinMany(false);
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"),
List.of(contact, teacher), List.of(getColumnList("contact"), getColumnList("teacher")));
// 断言:新增和删除以当前表格为准,不重复追加,新增临时编号不提交
String form = findFile(result, "/student-contact-form.vue");
assertTrue(form.contains("getTableData()"));
assertTrue(form.contains("inserted.has(row) ? { ...row, id: undefined } : { ...row }"));
assertFalse(form.contains("getRemoveRecords()"));
assertFalse(form.contains(".concat(insertRecords"));
assertFalse(form.contains("$dictType"));
assertTrue(form.contains("getStudentContactListByStudentId"));
if (frontType.name().endsWith("SCHEMA")) {
assertTrue(form.contains("<template #actions="));
}
result.forEach((path, content) -> {
if (path.endsWith(".vue") || path.endsWith("data.ts")) {
assertFalse(content.contains("#/api/infra/student"), path + ":API 导入必须匹配生成目录");
assertFalse(content.contains("VxeTableGridOptions<StudentApi.StudentContact>") && content.contains("import type { InfraStudentApi }"),
path + ":子表命名空间应与导入一致");
}
});
if (frontType == CodegenFrontTypeEnum.VUE3_VBEN5_ANTDV_NEXT_GENERAL) {
assertTrue(form.matches("(?s).*import\\s*\\{[^}]*\\bmessage\\b[^}]*}\\s*from 'antdv-next'.*"),
"子表校验提示必须导入实际的消息组件");
}
if (frontType == CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL) {
assertTrue(form.contains(":icon=\"Plus\""));
assertFalse(form.contains("h(Plus)"), "图标使用导入的组件,避免未绑定的 h");
}
assertTrue(form.contains("function validate(): boolean"));
assertTrue(form.contains("=== undefined"));
assertTrue(form.contains("loadData([])") || form.contains("list.value = []"));
String parentForm = findFile(result, "/modules/form.vue");
assertTrue(parentForm.contains("studentContactFormRef.value?.validate()"));
assertFalse(parentForm.contains("TODO 列表值校验"));
}
@ParameterizedTest
@EnumSource(value = CodegenTemplateTypeEnum.class, names = { "MASTER_ERP", "MASTER_INNER" })
public void testExecute_eleChildListImports(CodegenTemplateTypeEnum templateType) {
// 准备参数
CodegenFrontTypeEnum frontType = CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL;
CodegenTableDO table = getTable("student").setFrontType(frontType.getType())
.setTemplateType(templateType.getType());
CodegenTableDO contact = getTable("contact").setFrontType(frontType.getType())
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType()).setSubJoinColumnId(100L).setSubJoinMany(true);
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"),
List.of(contact), List.of(getColumnList("contact")));
// 断言:ERP 子表查询实际调用的工具必须导入
String index = findFile(result, "/index.vue");
assertTrue(index.contains("<VbenVxeTableToolbar"));
assertTrue(index.contains("</VbenVxeTableToolbar>"));
assertFalse(index.contains("<TableToolbar"));
String list = findFile(result, "/student-contact-list.vue");
if (templateType == CodegenTemplateTypeEnum.MASTER_ERP) {
assertTrue(list.contains("<VbenVxeTableToolbar"));
assertTrue(list.contains("</VbenVxeTableToolbar>"));
assertFalse(list.contains("</TableToolbar>"));
assertTrue(list.contains("cloneDeep(queryParams)"));
assertTrue(list.matches("(?s).*import\\s*\\{[^}]*\\bcloneDeep\\b[^}]*}\\s*from '@vben/utils'.*"));
}
assertTrue(list.contains("formatDateTime(row."));
assertTrue(list.matches("(?s).*import\\s*\\{[^}]*\\bformatDateTime\\b[^}]*}\\s*from '@vben/utils'.*"));
}
@Test
public void testExecute_vue3ImportUploadResponse() {
// 准备参数
codegenProperties.setImportEnable(true);
codegenEngine.initGlobalBindingMap();
CodegenTableDO table = getTable("student").setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
// 断言
String form = findFile(result, "/StudentImportForm.vue");
assertTrue(form.contains("const res = await StudentApi.importStudent(formData)"));
assertTrue(form.contains("const data = res.data"));
}
@Test
public void testExecute_vben2DateRangeFormat() {
// 准备参数
CodegenTableDO table = getTable("student").setFrontType(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
// 断言:查询区间提交后端需要的时间字符串
String data = findFile(result, ".data.ts");
assertTrue(data.contains("valueFormat: 'YYYY-MM-DD HH:mm:ss'"));
assertTrue(data.contains("allowClear: true"));
}
@Test
public void testExecute_uniappPaginationContract() {
// 准备参数
CodegenTableDO table = getTable("student").setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, getColumnList("student"), null, null);
// 断言:移动端已有分页/失败恢复契约保持有效,避免机械套用 PC 组件写法
String api = findFile(result, "/index.ts");
assertTrue(api.contains("params: PageParam"));
assertFalse(api.contains("PageReqVO"));
String list = result.entrySet().stream()
.filter(entry -> entry.getKey().endsWith("/index.vue") && entry.getValue().contains("<z-paging"))
.findFirst().orElseThrow().getValue();
assertTrue(list.contains("completeByTotal(data.list, data.total)"));
assertTrue(list.contains("complete(false)"));
assertTrue(list.contains("uni.$off("));
}
private static String findFile(Map<String, String> result, String suffix) {
return result.entrySet().stream().filter(entry -> entry.getKey().endsWith(suffix))
.findFirst().orElseThrow(() -> new AssertionError("缺少生成文件:" + suffix)).getValue();
}
}
@@ -35,12 +35,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -136,7 +135,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -72,7 +72,11 @@ const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
// 校验子表单
try {
if (!studentContactFormRef.value?.validate()) {
subTabsName.value = 'studentContact';
return;
}
try {
await studentTeacherFormRef.value?.validate()
} catch (e) {
subTabsName.value = 'studentTeacher'
@@ -31,12 +31,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -132,7 +131,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -32,25 +32,96 @@ async function handleDelete(row: StudentApi.StudentContact) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): StudentApi.StudentContact[] => {
const data = list.value as StudentApi.StudentContact[];
const removeRecords = tableRef.value?.getRemoveRecords() as StudentApi.StudentContact[];
const insertRecords = tableRef.value?.getInsertRecords() as StudentApi.StudentContact[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
];
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): StudentApi.StudentContact[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
message.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
message.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
message.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
message.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
message.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
message.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
message.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -86,7 +157,7 @@ watch(
<template #default="{ row }">
<Select v-model:value="row.sex" placeholder="请选择性别">
<Select.Option
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number')"
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
>
@@ -99,7 +170,7 @@ watch(
<template #default="{ row }">
<RadioGroup v-model:value="row.enabled">
<Radio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'boolean')"
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
>
@@ -51,6 +51,18 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
formData.value = {
id: undefined,
studentId: undefined,
name: undefined,
description: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: undefined,
video: undefined,
memo: undefined,
};
if (!val) {
return;
}
@@ -72,7 +72,11 @@ const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
// 校验子表单
try {
if (!studentContactFormRef.value?.validate()) {
subTabsName.value = 'studentContact';
return;
}
try {
await studentTeacherFormRef.value?.validate()
} catch (e) {
subTabsName.value = 'studentTeacher'
@@ -27,12 +27,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -128,7 +127,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -32,25 +32,96 @@ async function handleDelete(row: StudentApi.StudentContact) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): StudentApi.StudentContact[] => {
const data = list.value as StudentApi.StudentContact[];
const removeRecords = tableRef.value?.getRemoveRecords() as StudentApi.StudentContact[];
const insertRecords = tableRef.value?.getInsertRecords() as StudentApi.StudentContact[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
];
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): StudentApi.StudentContact[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
message.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
message.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
message.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
message.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
message.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
message.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
message.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -86,7 +157,7 @@ watch(
<template #default="{ row }">
<Select v-model:value="row.sex" placeholder="请选择性别">
<Select.Option
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number')"
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
>
@@ -99,7 +170,7 @@ watch(
<template #default="{ row }">
<RadioGroup v-model:value="row.enabled">
<Radio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'boolean')"
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
>
@@ -51,6 +51,18 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
formData.value = {
id: undefined,
studentId: undefined,
name: undefined,
description: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: undefined,
video: undefined,
memo: undefined,
};
if (!val) {
return;
}
@@ -27,12 +27,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -128,7 +127,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -48,6 +48,11 @@ function beforeUpload(file: FileType) {
return false;
}
/** 移除文件 */
function handleRemove() {
fileRef.value = null;
}
/** 下载导入模板 */
async function handleDownload() {
const data = await importStudentTemplate();
@@ -58,7 +63,12 @@ async function handleDownload() {
<template>
<Modal title="导入学生" class="w-1/3">
<div class="mx-4">
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
<Upload
:max-count="1"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
:on-remove="handleRemove"
>
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
</div>
@@ -28,12 +28,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -138,7 +137,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -24,7 +24,7 @@ const loading = ref(true) // 列表的加载中
const list = ref<any[]>([]) // 树列表的数据
const queryParams = reactive({
name: undefined,
name: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -91,7 +91,8 @@ async function handleDelete(row: CategoryApi.Category) {
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportCategory(queryParams);
const params = cloneDeep(queryParams) as any;
const data = await exportCategory(params);
downloadFileFromBlobPart({ fileName: '分类.xls', source: data });
}finally {
exportLoading.value = false;
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Tabs } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createStudent, getStudent, updateStudent } from '#/api/infra/demo';
@@ -269,7 +269,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
minWidth: 120,
slots: { default: 'sex' },
params: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'boolean'),
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
},
},
{
@@ -309,7 +309,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
}
/** 列表的字段 */
export function useStudentContactGridColumns(): VxeTableGridOptions<StudentApi.StudentContact>['columns'] {
export function useStudentContactGridColumns(): VxeTableGridOptions<InfraStudentApi.StudentContact>['columns'] {
return [
{
field: 'id',
@@ -490,7 +490,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
}
/** 列表的字段 */
export function useStudentTeacherGridColumns(): VxeTableGridOptions<StudentApi.StudentTeacher>['columns'] {
export function useStudentTeacherGridColumns(): VxeTableGridOptions<InfraStudentApi.StudentTeacher>['columns'] {
return [
{
field: 'id',
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createStudent, getStudent, updateStudent } from '#/api/infra/demo';
@@ -48,7 +48,12 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 校验子表单
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
const studentContactValid = await studentContactFormRef.value?.validate();
if (!studentContactValid) {
subTabsName.value = 'studentContact';
return;
}
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
if (!studentTeacherValid) {
subTabsName.value = 'studentTeacher';
return;
@@ -6,7 +6,7 @@
import { $t } from '#/locales';
import { Plus } from "@vben/icons";
import { Button, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'ant-design-vue';
import { Button, Checkbox, CheckboxGroup, DatePicker, Input, message, RadioGroup, Select, Tabs, Textarea } from 'ant-design-vue';
import { ImageUpload, FileUpload } from "#/components/upload";
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { useStudentContactGridEditColumns } from '../data';
@@ -45,26 +45,96 @@ const handleDelete = async (row: InfraStudentApi.StudentContact) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): InfraStudentApi.StudentContact[] => {
const data = gridApi.grid.getData() as InfraStudentApi.StudentContact[];
const removeRecords = gridApi.grid.getRemoveRecords() as InfraStudentApi.StudentContact[];
const insertRecords = gridApi.grid.getInsertRecords() as InfraStudentApi.StudentContact[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): InfraStudentApi.StudentContact[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
message.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
message.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
message.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
message.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
message.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
message.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
message.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await getStudentContactListByStudentId(props.studentId!));
},
{ immediate: true },
@@ -39,10 +39,11 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await getStudentTeacherByStudentId(props.studentId!));
},
{ immediate: true },
@@ -268,7 +268,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
minWidth: 120,
slots: { default: 'sex' },
params: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'boolean'),
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
},
},
{
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createStudent, getStudent, updateStudent } from '#/api/infra/demo';
@@ -48,7 +48,12 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 校验子表单
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
const studentContactValid = await studentContactFormRef.value?.validate();
if (!studentContactValid) {
subTabsName.value = 'studentContact';
return;
}
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
if (!studentTeacherValid) {
subTabsName.value = 'studentTeacher';
return;
@@ -6,7 +6,7 @@
import { $t } from '#/locales';
import { Plus } from "@vben/icons";
import { Button, Tabs, Checkbox, Input, Textarea, Select,RadioGroup,CheckboxGroup, DatePicker } from 'ant-design-vue';
import { Button, Checkbox, CheckboxGroup, DatePicker, Input, message, RadioGroup, Select, Tabs, Textarea } from 'ant-design-vue';
import { ImageUpload, FileUpload } from "#/components/upload";
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { useStudentContactGridEditColumns } from '../data';
@@ -45,26 +45,96 @@ const handleDelete = async (row: InfraStudentApi.StudentContact) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): InfraStudentApi.StudentContact[] => {
const data = gridApi.grid.getData() as InfraStudentApi.StudentContact[];
const removeRecords = gridApi.grid.getRemoveRecords() as InfraStudentApi.StudentContact[];
const insertRecords = gridApi.grid.getInsertRecords() as InfraStudentApi.StudentContact[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): InfraStudentApi.StudentContact[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
message.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
message.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
message.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
message.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
message.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
message.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
message.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await getStudentContactListByStudentId(props.studentId!));
},
{ immediate: true },
@@ -39,10 +39,11 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await getStudentTeacherByStudentId(props.studentId!));
},
{ immediate: true },
@@ -51,6 +51,11 @@ function beforeUpload(file: FileType) {
return false;
}
/** 移除文件 */
function handleRemove() {
formApi.setFieldValue('file', undefined);
}
/** 下载模板 */
async function handleDownload() {
const data = await importStudentTemplate();
@@ -67,6 +72,7 @@ async function handleDownload() {
:max-count="1"
accept=".xls,.xlsx"
:before-upload="beforeUpload"
:on-remove="handleRemove"
>
<Button type="primary"> 选择 Excel 文件 </Button>
</Upload>
@@ -32,7 +32,7 @@ export function useFormSchema(): VbenFormSchema[] {
id: 0,
name: '顶级分类',
});
return handleTree(data);
return handleTree(data, 'id', 'parentId');
},
labelField: 'name',
valueField: 'id',
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -11,7 +11,7 @@ import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, El
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { getStudent, createStudent, updateStudent } from '#/api/infra/student';
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
const emit = defineEmits(['success']);
@@ -102,56 +102,56 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</Modal>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -19,7 +19,7 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { isEmpty } from '@vben/utils';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/student';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
/** 子表的列表 */
const subTabsName = ref('studentContact')
@@ -35,12 +35,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -134,7 +133,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -156,61 +162,61 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -219,24 +225,24 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="学生">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -244,8 +250,8 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -254,8 +260,8 @@ onMounted(() => {
v-access:code="['infra:student:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -264,10 +270,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
@cell-click="onCellClick"
@@ -281,36 +287,36 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn type="checkbox" width="40"></VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -318,8 +324,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -328,13 +334,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -347,14 +353,14 @@ onMounted(() => {
</ContentWrap>
<ContentWrap>
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<el-tab-pane name="studentContact" label="学生联系人">
<ElTabs v-model="subTabsName">
<ElTabPane name="studentContact" label="学生联系人">
<StudentContactList :student-id="selectStudent?.id" />
</el-tab-pane>
<el-tab-pane name="studentTeacher" label="学生班主任">
</ElTabPane>
<ElTabPane name="studentTeacher" label="学生班主任">
<StudentTeacherList :student-id="selectStudent?.id" />
</el-tab-pane>
</el-tabs>
</ElTabPane>
</ElTabs>
</ContentWrap>
</Page>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { computed, ref, reactive } from 'vue';
@@ -13,7 +13,7 @@
import { $t } from '#/locales';
import { getStudentContact, createStudentContact, updateStudentContact } from '#/api/infra/student';
import { getStudentContact, createStudentContact, updateStudentContact } from '#/api/infra/demo';
const emit = defineEmits(['success']);
const getTitle = computed(() => {
@@ -107,56 +107,56 @@
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</Modal>
</template>
@@ -1,11 +1,12 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
@@ -21,8 +22,8 @@
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
import { deleteStudentContact, deleteStudentContactList, getStudentContactPage } from '#/api/infra/student';
import { isEmpty } from '@vben/utils';
import { deleteStudentContact, deleteStudentContactList, getStudentContactPage } from '#/api/infra/demo';
import { cloneDeep, isEmpty } from '@vben/utils';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
@@ -159,61 +160,61 @@ onMounted(() => {
<div class="h-[600px]">
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -222,14 +223,14 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
@@ -239,7 +240,7 @@ onMounted(() => {
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -247,8 +248,8 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -257,10 +258,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -268,37 +269,37 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="studentId" title="学生编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn type="checkbox" width="40"></VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="studentId" title="学生编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -306,8 +307,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -316,13 +317,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { computed, ref, reactive } from 'vue';
@@ -13,7 +13,7 @@
import { $t } from '#/locales';
import { getStudentTeacher, createStudentTeacher, updateStudentTeacher } from '#/api/infra/student';
import { getStudentTeacher, createStudentTeacher, updateStudentTeacher } from '#/api/infra/demo';
const emit = defineEmits(['success']);
const getTitle = computed(() => {
@@ -107,56 +107,56 @@
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</Modal>
</template>
@@ -1,11 +1,12 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
@@ -21,8 +22,8 @@
import { Plus, Trash2 } from '@vben/icons';
import { $t } from '#/locales';
import { deleteStudentTeacher, deleteStudentTeacherList, getStudentTeacherPage } from '#/api/infra/student';
import { isEmpty } from '@vben/utils';
import { deleteStudentTeacher, deleteStudentTeacherList, getStudentTeacherPage } from '#/api/infra/demo';
import { cloneDeep, isEmpty } from '@vben/utils';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
@@ -159,61 +160,61 @@ onMounted(() => {
<div class="h-[600px]">
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -222,14 +223,14 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
@@ -239,7 +240,7 @@ onMounted(() => {
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -247,8 +248,8 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -257,10 +258,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -268,37 +269,37 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="studentId" title="学生编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn type="checkbox" width="40"></VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="studentId" title="学生编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -306,8 +307,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -316,13 +317,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -13,7 +13,7 @@ import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, El
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { getStudent, createStudent, updateStudent } from '#/api/infra/student';
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
const emit = defineEmits(['success']);
@@ -71,7 +71,11 @@ const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
// 校验子表单
try {
if (!studentContactFormRef.value?.validate()) {
subTabsName.value = 'studentContact';
return;
}
try {
await studentTeacherFormRef.value?.validate()
} catch (e) {
subTabsName.value = 'studentTeacher'
@@ -118,65 +122,65 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<el-tab-pane name="studentContact" label="学生联系人">
<ElTabs v-model="subTabsName">
<ElTabPane name="studentContact" label="学生联系人">
<StudentContactForm ref="studentContactFormRef" :student-id="formData?.id" />
</el-tab-pane>
<el-tab-pane name="studentTeacher" label="学生班主任">
</ElTabPane>
<ElTabPane name="studentTeacher" label="学生班主任">
<StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData?.id" />
</el-tab-pane>
</el-tabs>
</ElTabPane>
</ElTabs>
</Modal>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -19,7 +19,7 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { isEmpty } from '@vben/utils';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/student';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
/** 子表的列表 */
const subTabsName = ref('studentContact')
@@ -31,12 +31,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -130,7 +129,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -152,61 +158,61 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -215,24 +221,24 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="学生">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -240,8 +246,8 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -250,8 +256,8 @@ onMounted(() => {
v-access:code="['infra:student:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -260,10 +266,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -271,50 +277,50 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<VxeColumn type="checkbox" width="40"></VxeColumn>
<!-- 子表的列表 -->
<vxe-column type="expand" width="60">
<VxeColumn type="expand" width="60">
<template #content="{ row }">
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName" class="mx-8">
<el-tab-pane name="studentContact" label="学生联系人">
<ElTabs v-model="subTabsName" class="mx-8">
<ElTabPane name="studentContact" label="学生联系人">
<StudentContactList :student-id="row?.id" />
</el-tab-pane>
<el-tab-pane name="studentTeacher" label="学生班主任">
</ElTabPane>
<ElTabPane name="studentTeacher" label="学生班主任">
<StudentTeacherList :student-id="row?.id" />
</el-tab-pane>
</el-tabs>
</ElTabPane>
</ElTabs>
</template>
</vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
</VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -322,8 +328,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -332,13 +338,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -1,19 +1,28 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import type { StudentApi } from '#/api/infra/demo';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { ref, watch } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Plus } from '@vben/icons';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import {
ElButton,
ElDatePicker,
ElInput,
ElMessage,
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from 'element-plus';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { getStudentContactListByStudentId } from '#/api/infra/demo';
import { $t } from '#/locales';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { Plus } from "@vben/icons";
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { getStudentContactListByStudentId } from '#/api/infra/student';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
}>()
@@ -30,24 +39,96 @@ async function onDelete(row: StudentApi.StudentContact) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): StudentApi.StudentContact[] => {
const data = list.value as StudentApi.StudentContact[];
const removeRecords = tableRef.value?.getRemoveRecords() as StudentApi.StudentContact[];
const insertRecords = tableRef.value?.getInsertRecords() as StudentApi.StudentContact[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): StudentApi.StudentContact[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -58,70 +139,70 @@ watch(
</script>
<template>
<vxe-table ref="tableRef" :data="list" show-overflow class="mx-4">
<vxe-column field="name" title="名字" align="center">
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
<VxeColumn field="name" title="名字" align="center">
<template #default="{row}">
<el-input v-model="row.name" />
<ElInput v-model="row.name" />
</template>
</vxe-column>
<vxe-column field="description" title="简介" align="center">
</VxeColumn>
<VxeColumn field="description" title="简介" align="center">
<template #default="{row}">
<el-input v-model="row.description" type="textarea" />
<ElInput v-model="row.description" type="textarea" />
</template>
</vxe-column>
<vxe-column field="birthday" title="出生日期" align="center">
</VxeColumn>
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
<el-date-picker
<ElDatePicker
v-model="row.birthday"
type="datetime"
value-format="x"
format="YYYY-MM-DD HH:mm:ss"
/>
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<el-select v-model="row.sex" placeholder="请选择性别">
<el-option
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number')"
<ElSelect v-model="row.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</ElSelect>
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<el-radio-group v-model="row.enabled">
<el-radio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'boolean')"
<ElRadioGroup v-model="row.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</ElRadio>
</ElRadioGroup>
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center">
<template #default="{row}">
<ImageUpload v-model="row.avatar" />
</template>
</vxe-column>
<vxe-column field="video" title="附件" align="center">
</VxeColumn>
<VxeColumn field="video" title="附件" align="center">
<template #default="{row}">
<FileUpload v-model="row.video" />
</template>
</vxe-column>
<vxe-column field="memo" title="备注" align="center">
</VxeColumn>
<VxeColumn field="memo" title="备注" align="center">
<template #default="{row}">
<el-input v-model="row.memo" type="textarea" />
<ElInput v-model="row.memo" type="textarea" />
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="danger"
link
@@ -129,13 +210,13 @@ watch(
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<div class="flex justify-center mt-4">
<el-button :icon="h(Plus)" type="primary" plain @click="handleAdd" v-access:code="['infra:student:create']">
<ElButton :icon="Plus" type="primary" plain @click="handleAdd" v-access:code="['infra:student:create']">
{{ $t('ui.actionTitle.create', ['学生联系人']) }}
</el-button>
</ElButton>
</div>
</template>
@@ -1,11 +1,12 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
@@ -13,7 +14,7 @@
import { ContentWrap } from '#/components/content-wrap';
import { getStudentContactListByStudentId } from '#/api/infra/student';
import { getStudentContactListByStudentId } from '#/api/infra/demo';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
@@ -52,38 +53,38 @@ watch(
<template>
<ContentWrap title="学生联系人列表">
<vxe-table
<VxeTable
:data="list"
show-overflow
:loading="loading"
>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="studentId" title="学生编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="studentId" title="学生编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
</ContentWrap>
</template>
@@ -1,17 +1,26 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { FormRules } from 'element-plus';
import type { StudentApi } from '#/api/infra/demo';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { nextTick, reactive, ref, watch } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { $t } from '#/locales';
import {
ElForm,
ElFormItem,
ElDatePicker,
ElInput,
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from 'element-plus';
import type { FormRules } from 'element-plus';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { getStudentTeacherByStudentId } from '#/api/infra/student';
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
import { $t } from '#/locales';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
@@ -50,6 +59,18 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
formData.value = {
id: undefined,
studentId: undefined,
name: undefined,
description: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: undefined,
video: undefined,
memo: undefined,
};
if (!val) {
return;
}
@@ -61,7 +82,7 @@ watch(
</script>
<template>
<el-form
<ElForm
ref="formRef"
class="mx-4"
:model="formData"
@@ -69,48 +90,48 @@ watch(
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</template>
@@ -1,11 +1,12 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { reactive,ref, h, nextTick,watch,onMounted } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { getRangePickerDefaultProps } from '#/utils';
@@ -13,7 +14,7 @@
import { ContentWrap } from '#/components/content-wrap';
import { getStudentTeacherByStudentId } from '#/api/infra/student';
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
@@ -52,38 +53,38 @@ watch(
<template>
<ContentWrap title="学生班主任列表">
<vxe-table
<VxeTable
:data="list"
show-overflow
:loading="loading"
>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="studentId" title="学生编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="studentId" title="学生编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
</ContentWrap>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -13,7 +13,7 @@ import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, El
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { getStudent, createStudent, updateStudent } from '#/api/infra/student';
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
const emit = defineEmits(['success']);
@@ -71,7 +71,11 @@ const [Modal, modalApi] = useVbenModal({
async onConfirm() {
await formRef.value?.validate();
// 校验子表单
try {
if (!studentContactFormRef.value?.validate()) {
subTabsName.value = 'studentContact';
return;
}
try {
await studentTeacherFormRef.value?.validate()
} catch (e) {
subTabsName.value = 'studentTeacher'
@@ -118,65 +122,65 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<el-tab-pane name="studentContact" label="学生联系人">
<ElTabs v-model="subTabsName">
<ElTabPane name="studentContact" label="学生联系人">
<StudentContactForm ref="studentContactFormRef" :student-id="formData?.id" />
</el-tab-pane>
<el-tab-pane name="studentTeacher" label="学生班主任">
</ElTabPane>
<ElTabPane name="studentTeacher" label="学生班主任">
<StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData?.id" />
</el-tab-pane>
</el-tabs>
</ElTabPane>
</ElTabs>
</Modal>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -17,7 +17,7 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { isEmpty } from '@vben/utils';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/student';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
const loading = ref(true) // 列表的加载中
@@ -27,12 +27,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -126,7 +125,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -148,61 +154,61 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -211,24 +217,24 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="学生">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -236,8 +242,8 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -246,8 +252,8 @@ onMounted(() => {
v-access:code="['infra:student:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -256,10 +262,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -267,36 +273,36 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn type="checkbox" width="40"></VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -304,8 +310,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -314,13 +320,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -1,19 +1,28 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import type { StudentApi } from '#/api/infra/demo';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { ref, watch } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Plus } from '@vben/icons';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import {
ElButton,
ElDatePicker,
ElInput,
ElMessage,
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from 'element-plus';
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { getStudentContactListByStudentId } from '#/api/infra/demo';
import { $t } from '#/locales';
import type { VxeTableInstance } from '#/adapter/vxe-table';
import { Plus } from "@vben/icons";
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { getStudentContactListByStudentId } from '#/api/infra/student';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
}>()
@@ -30,24 +39,96 @@ async function onDelete(row: StudentApi.StudentContact) {
await tableRef.value?.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): StudentApi.StudentContact[] => {
const data = list.value as StudentApi.StudentContact[];
const removeRecords = tableRef.value?.getRemoveRecords() as StudentApi.StudentContact[];
const insertRecords = tableRef.value?.getInsertRecords() as StudentApi.StudentContact[];
return [
...data.filter(
(row) => !removeRecords.some((removed) => removed.id === row.id),
),
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): StudentApi.StudentContact[] {
const table = tableRef.value;
if (!table) {
return [];
}
const { fullData } = table.getTableData();
const inserted = new Set(table.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
list.value = [];
if (!val) {
return;
}
@@ -58,70 +139,70 @@ watch(
</script>
<template>
<vxe-table ref="tableRef" :data="list" show-overflow class="mx-4">
<vxe-column field="name" title="名字" align="center">
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
<VxeColumn field="name" title="名字" align="center">
<template #default="{row}">
<el-input v-model="row.name" />
<ElInput v-model="row.name" />
</template>
</vxe-column>
<vxe-column field="description" title="简介" align="center">
</VxeColumn>
<VxeColumn field="description" title="简介" align="center">
<template #default="{row}">
<el-input v-model="row.description" type="textarea" />
<ElInput v-model="row.description" type="textarea" />
</template>
</vxe-column>
<vxe-column field="birthday" title="出生日期" align="center">
</VxeColumn>
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
<el-date-picker
<ElDatePicker
v-model="row.birthday"
type="datetime"
value-format="x"
format="YYYY-MM-DD HH:mm:ss"
/>
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<el-select v-model="row.sex" placeholder="请选择性别">
<el-option
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number')"
<ElSelect v-model="row.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</ElSelect>
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<el-radio-group v-model="row.enabled">
<el-radio
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'boolean')"
<ElRadioGroup v-model="row.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</ElRadio>
</ElRadioGroup>
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center">
<template #default="{row}">
<ImageUpload v-model="row.avatar" />
</template>
</vxe-column>
<vxe-column field="video" title="附件" align="center">
</VxeColumn>
<VxeColumn field="video" title="附件" align="center">
<template #default="{row}">
<FileUpload v-model="row.video" />
</template>
</vxe-column>
<vxe-column field="memo" title="备注" align="center">
</VxeColumn>
<VxeColumn field="memo" title="备注" align="center">
<template #default="{row}">
<el-input v-model="row.memo" type="textarea" />
<ElInput v-model="row.memo" type="textarea" />
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="danger"
link
@@ -129,13 +210,13 @@ watch(
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<div class="flex justify-center mt-4">
<el-button :icon="h(Plus)" type="primary" plain @click="handleAdd" v-access:code="['infra:student:create']">
<ElButton :icon="Plus" type="primary" plain @click="handleAdd" v-access:code="['infra:student:create']">
{{ $t('ui.actionTitle.create', ['学生联系人']) }}
</el-button>
</ElButton>
</div>
</template>
@@ -1,17 +1,26 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { FormRules } from 'element-plus';
import type { StudentApi } from '#/api/infra/demo';
import { computed, ref, reactive, h, onMounted,watch,nextTick } from 'vue';
import { nextTick, reactive, ref, watch } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElButton, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker } from 'element-plus';
import { $t } from '#/locales';
import {
ElForm,
ElFormItem,
ElDatePicker,
ElInput,
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from 'element-plus';
import type { FormRules } from 'element-plus';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { getStudentTeacherByStudentId } from '#/api/infra/student';
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
import { $t } from '#/locales';
const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
@@ -50,6 +59,18 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
formData.value = {
id: undefined,
studentId: undefined,
name: undefined,
description: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
avatar: undefined,
video: undefined,
memo: undefined,
};
if (!val) {
return;
}
@@ -61,7 +82,7 @@ watch(
</script>
<template>
<el-form
<ElForm
ref="formRef"
class="mx-4"
:model="formData"
@@ -69,48 +90,48 @@ watch(
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -11,7 +11,7 @@ import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, El
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { getStudent, createStudent, updateStudent } from '#/api/infra/student';
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
const emit = defineEmits(['success']);
@@ -102,56 +102,56 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</Modal>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -17,7 +17,7 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { isEmpty } from '@vben/utils';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/student';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
const loading = ref(true) // 列表的加载中
@@ -27,12 +27,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -126,7 +125,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -148,61 +154,61 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -211,24 +217,24 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="学生">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -236,8 +242,8 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -246,8 +252,8 @@ onMounted(() => {
v-access:code="['infra:student:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -256,10 +262,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -267,36 +273,36 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn type="checkbox" width="40"></VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -304,8 +310,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -314,13 +320,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -11,7 +11,7 @@ import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, El
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { getStudent, createStudent, updateStudent } from '#/api/infra/student';
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
const emit = defineEmits(['success']);
@@ -102,56 +102,56 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
</el-form-item>
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="简介" prop="description">
<ElInput v-model="formData.description" type="textarea" placeholder="请输入简介" />
</ElFormItem>
<ElFormItem label="出生日期" prop="birthday">
<ElDatePicker
v-model="formData.birthday"
value-format="x"
placeholder="选择出生日期"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="请选择性别">
<el-option
</ElFormItem>
<ElFormItem label="性别" prop="sex">
<ElSelect v-model="formData.sex" placeholder="请选择性别">
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效" prop="enabled">
<ElRadioGroup v-model="formData.enabled">
<ElRadio
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:label="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="头像" prop="avatar">
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="头像" prop="avatar">
<ImageUpload v-model="formData.avatar" />
</el-form-item>
<el-form-item label="附件" prop="video">
</ElFormItem>
<ElFormItem label="附件" prop="video">
<FileUpload v-model="formData.video" />
</el-form-item>
<el-form-item label="备注" prop="memo">
</ElFormItem>
<ElFormItem label="备注" prop="memo">
<RichTextarea v-model="formData.memo" height="500px" />
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</Modal>
</template>
@@ -47,6 +47,16 @@ function handleChange(file: any) {
}
}
/** 移除文件 */
function handleRemove() {
fileRef.value = null;
}
/** 超出文件数量 */
function handleExceed() {
ElMessage.warning('只能选择一个文件,请先移除已选文件');
}
/** 下载导入模板 */
async function handleDownload() {
const data = await importStudentTemplate();
@@ -61,6 +71,8 @@ async function handleDownload() {
:limit="1"
accept=".xls,.xlsx"
:on-change="handleChange"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:auto-upload="false"
>
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { StudentApi } from '#/api/infra/student';
import type { StudentApi } from '#/api/infra/demo';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -17,7 +17,7 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { isEmpty } from '@vben/utils';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/student';
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
import StudentImport from './modules/import-form.vue';
@@ -28,12 +28,11 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
birthday: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
name: undefined,
birthday: undefined,
sex: undefined,
enabled: undefined,
createTime: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -136,7 +135,14 @@ function handleRowCheckboxChange({
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportStudent(queryParams);
const params = cloneDeep(queryParams) as any;
if (params.birthday && Array.isArray(params.birthday)) {
params.birthday = (params.birthday as string[]).join(',');
}
if (params.createTime && Array.isArray(params.createTime)) {
params.createTime = (params.createTime as string[]).join(',');
}
const data = await exportStudent(params);
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
}finally {
exportLoading.value = false;
@@ -159,61 +165,61 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
</ElFormItem>
<ElFormItem label="出生日期">
<ElDatePicker
v-model="queryParams.birthday"
value-format="YYYY-MM-DD"
placeholder="选择出生日期"
clearable
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
</ElFormItem>
<ElFormItem label="性别">
<ElSelect
v-model="queryParams.sex"
placeholder="请选择性别"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效">
<el-select
</ElSelect>
</ElFormItem>
<ElFormItem label="是否有效">
<ElSelect
v-model="queryParams.enabled"
placeholder="请选择是否有效"
clearable
class="!w-[240px]"
>
<el-option
<ElOption
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
:key="dict.value"
:value="dict.value"
:label="dict.label"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
</ElSelect>
</ElFormItem>
<ElFormItem label="创建时间">
<ElDatePicker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD"
@@ -222,24 +228,24 @@ onMounted(() => {
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="学生">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -247,16 +253,16 @@ onMounted(() => {
v-access:code="['infra:student:create']"
>
{{ $t('ui.actionTitle.create', ['学生']) }}
</el-button>
<el-button
</ElButton>
<ElButton
class="ml-2"
type="primary"
@click="handleImport"
v-access:code="['infra:student:import']"
>
导入
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -265,8 +271,8 @@ onMounted(() => {
v-access:code="['infra:student:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Trash2)"
type="danger"
class="ml-2"
@@ -275,10 +281,10 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
批量删除
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
show-overflow
@@ -286,36 +292,36 @@ onMounted(() => {
@checkboxAll="handleRowCheckboxChange"
@checkboxChange="handleRowCheckboxChange"
>
<vxe-column type="checkbox" width="40"></vxe-column>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="name" title="名字" align="center" />
<vxe-column field="description" title="简介" align="center" />
<vxe-column field="birthday" title="出生日期" align="center">
<VxeColumn type="checkbox" width="40"></VxeColumn>
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="name" title="名字" align="center" />
<VxeColumn field="description" title="简介" align="center" />
<VxeColumn field="birthday" title="出生日期" align="center">
<template #default="{row}">
{{formatDateTime(row.birthday)}}
</template>
</vxe-column>
<vxe-column field="sex" title="性别" align="center">
</VxeColumn>
<VxeColumn field="sex" title="性别" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
</template>
</vxe-column>
<vxe-column field="enabled" title="是否有效" align="center">
</VxeColumn>
<VxeColumn field="enabled" title="是否有效" align="center">
<template #default="{row}">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
</template>
</vxe-column>
<vxe-column field="avatar" title="头像" align="center" />
<vxe-column field="video" title="附件" align="center" />
<vxe-column field="memo" title="备注" align="center" />
<vxe-column field="createTime" title="创建时间" align="center">
</VxeColumn>
<VxeColumn field="avatar" title="头像" align="center" />
<VxeColumn field="video" title="附件" align="center" />
<VxeColumn field="memo" title="备注" align="center" />
<VxeColumn field="createTime" title="创建时间" align="center">
<template #default="{row}">
{{formatDateTime(row.createTime)}}
</template>
</vxe-column>
<vxe-column field="operation" title="操作" align="center">
</VxeColumn>
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -323,8 +329,8 @@ onMounted(() => {
v-access:code="['infra:student:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -333,13 +339,13 @@ onMounted(() => {
v-access:code="['infra:student:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
<!-- 分页 -->
<div class="mt-2 flex justify-end">
<el-pagination
<ElPagination
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { CategoryApi } from '#/api/infra/category';
import type { CategoryApi } from '#/api/infra/demo';
import type { FormRules } from 'element-plus';
import { useVbenModal } from '@vben/common-ui';
@@ -7,12 +7,12 @@ import { getDictOptions } from '@vben/hooks';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { ImageUpload, FileUpload } from "#/components/upload";
import { ElMessage, ElTabs, ElTabPane, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElRadioGroup, ElRadio, ElCheckboxGroup, ElCheckbox, ElDatePicker, ElTreeSelect } from 'element-plus';
import { getCategoryList } from '#/api/infra/category';
import { getCategoryList } from '#/api/infra/demo';
import { handleTree } from '@vben/utils'
import { computed, ref, reactive } from 'vue';
import { $t } from '#/locales';
import { getCategory, createCategory, updateCategory } from '#/api/infra/category';
import { getCategory, createCategory, updateCategory } from '#/api/infra/demo';
const emit = defineEmits(['success']);
@@ -98,18 +98,18 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal :title="getTitle">
<el-form
<ElForm
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入名字" />
</el-form-item>
<el-form-item label="父编号" prop="parentId">
<el-tree-select
<ElFormItem label="名字" prop="name">
<ElInput v-model="formData.name" placeholder="请输入名字" />
</ElFormItem>
<ElFormItem label="父编号" prop="parentId">
<ElTreeSelect
v-model="formData.parentId"
:data="categoryTree"
:props="{
@@ -121,7 +121,7 @@ const [Modal, modalApi] = useVbenModal({
default-expand-all
placeholder="请选择父编号"
/>
</el-form-item>
</el-form>
</ElFormItem>
</ElForm>
</Modal>
</template>
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { CategoryApi } from '#/api/infra/category';
import type { CategoryApi } from '#/api/infra/demo';
import { ref, h, reactive, onMounted, nextTick } from 'vue';
@@ -16,14 +16,14 @@ import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { handleTree,isEmpty } from '@vben/utils'
import { getCategoryList, deleteCategory, exportCategory } from '#/api/infra/category';
import { getCategoryList, deleteCategory, exportCategory } from '#/api/infra/demo';
const loading = ref(true) // 列表的加载中
const list = ref<any[]>([]) // 树列表的数据
const queryParams = reactive({
name: undefined,
name: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
@@ -89,7 +89,8 @@ async function handleDelete(row: CategoryApi.Category) {
async function handleExport() {
try {
exportLoading.value = true;
const data = await exportCategory(queryParams);
const params = cloneDeep(queryParams) as any;
const data = await exportCategory(params);
downloadFileFromBlobPart({ fileName: '分类.xls', source: data });
}finally {
exportLoading.value = false;
@@ -117,40 +118,40 @@ onMounted(() => {
<ContentWrap v-if="!hiddenSearchBar">
<!-- 搜索工作栏 -->
<el-form
<ElForm
:model="queryParams"
ref="queryFormRef"
inline
>
<el-form-item label="名字">
<el-input
<ElFormItem label="名字">
<ElInput
v-model="queryParams.name"
placeholder="请输入名字"
clearable
@keyup.enter="handleQuery"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button class="ml-2" @click="resetQuery"> 重置 </el-button>
<el-button class="ml-2" @click="handleQuery" type="primary">
</ElFormItem>
<ElFormItem>
<ElButton class="ml-2" @click="resetQuery"> 重置 </ElButton>
<ElButton class="ml-2" @click="handleQuery" type="primary">
搜索
</el-button>
</el-form-item>
</el-form>
</ElButton>
</ElFormItem>
</ElForm>
</ContentWrap>
<!-- 列表 -->
<ContentWrap title="分类">
<template #extra>
<TableToolbar
<VbenVxeTableToolbar
ref="tableToolbarRef"
v-model:hidden-search="hiddenSearchBar"
>
<el-button @click="handleExpand" class="mr-2">
<ElButton @click="handleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</el-button>
<el-button
</ElButton>
<ElButton
class="ml-2"
:icon="h(Plus)"
type="primary"
@@ -158,8 +159,8 @@ onMounted(() => {
v-access:code="['infra:category:create']"
>
{{ $t('ui.actionTitle.create', ['分类']) }}
</el-button>
<el-button
</ElButton>
<ElButton
:icon="h(Download)"
type="primary"
class="ml-2"
@@ -168,10 +169,10 @@ onMounted(() => {
v-access:code="['infra:category:export']"
>
{{ $t('ui.actionTitle.export') }}
</el-button>
</TableToolbar>
</ElButton>
</VbenVxeTableToolbar>
</template>
<vxe-table
<VxeTable
ref="tableRef"
:data="list"
:tree-config="{
@@ -184,12 +185,12 @@ onMounted(() => {
show-overflow
:loading="loading"
>
<vxe-column field="id" title="编号" align="center" />
<vxe-column field="name" title="名字" align="center" tree-node/>
<vxe-column field="parentId" title="父编号" align="center" />
<vxe-column field="operation" title="操作" align="center">
<VxeColumn field="id" title="编号" align="center" />
<VxeColumn field="name" title="名字" align="center" tree-node/>
<VxeColumn field="parentId" title="父编号" align="center" />
<VxeColumn field="operation" title="操作" align="center">
<template #default="{row}">
<el-button
<ElButton
size="small"
type="primary"
link
@@ -197,8 +198,8 @@ onMounted(() => {
v-access:code="['infra:category:create']"
>
新增下级
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="primary"
link
@@ -206,8 +207,8 @@ onMounted(() => {
v-access:code="['infra:category:update']"
>
{{ $t('ui.actionTitle.edit') }}
</el-button>
<el-button
</ElButton>
<ElButton
size="small"
type="danger"
link
@@ -217,10 +218,10 @@ onMounted(() => {
v-access:code="['infra:category:delete']"
>
{{ $t('ui.actionTitle.delete') }}
</el-button>
</ElButton>
</template>
</vxe-column>
</vxe-table>
</VxeColumn>
</VxeTable>
</ContentWrap>
</Page>
</template>
@@ -42,7 +42,7 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -65,8 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -277,7 +276,7 @@ export function useStudentContactFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -300,8 +299,7 @@ export function useStudentContactFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -520,7 +518,7 @@ export function useStudentTeacherFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -543,8 +541,7 @@ export function useStudentTeacherFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage, ElTabs, ElTabPane } from 'element-plus';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { createStudent, getStudent, updateStudent } from '#/api/infra/demo';
@@ -42,7 +42,7 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -65,8 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -270,7 +269,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
minWidth: 120,
slots: { default: 'sex' },
params: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'boolean'),
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
},
},
{
@@ -310,7 +309,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
}
/** 列表的字段 */
export function useStudentContactGridColumns(): VxeTableGridOptions<StudentApi.StudentContact>['columns'] {
export function useStudentContactGridColumns(): VxeTableGridOptions<InfraStudentApi.StudentContact>['columns'] {
return [
{
field: 'id',
@@ -444,7 +443,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -467,8 +466,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -492,7 +490,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
}
/** 列表的字段 */
export function useStudentTeacherGridColumns(): VxeTableGridOptions<StudentApi.StudentTeacher>['columns'] {
export function useStudentTeacherGridColumns(): VxeTableGridOptions<InfraStudentApi.StudentTeacher>['columns'] {
return [
{
field: 'id',
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { ElMessage, ElTabPane, ElTabs } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { createStudent, getStudent, updateStudent } from '#/api/infra/demo';
@@ -48,7 +48,12 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 校验子表单
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
const studentContactValid = await studentContactFormRef.value?.validate();
if (!studentContactValid) {
subTabsName.value = 'studentContact';
return;
}
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
if (!studentTeacherValid) {
subTabsName.value = 'studentTeacher';
return;
@@ -6,10 +6,9 @@
import { $t } from '#/locales';
import { Plus } from "@vben/icons";
import { ElButton, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElOption, ElRadioGroup, ElCheckboxGroup, ElDatePicker } from 'element-plus';
import { ElButton, ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElMessage, ElOption, ElRadio, ElRadioGroup, ElSelect, ElTabPane, ElTabs } from 'element-plus';
import { ImageUpload, FileUpload } from "#/components/upload";
import type { OnActionClickParams } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { useStudentContactGridEditColumns } from '../data';
import { getStudentContactListByStudentId } from '#/api/infra/demo';
@@ -17,22 +16,9 @@ const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
}>()
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraStudentApi.StudentContact>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useStudentContactGridEditColumns(onActionClick),
columns: useStudentContactGridEditColumns(),
border: true,
showOverflow: true,
autoResize: true,
@@ -51,7 +37,7 @@ gridOptions: {
/** 添加学生联系人 */
const onAdd = async () => {
await gridApi.grid.insertAt({} as StudentApi.StudentContact, -1);
await gridApi.grid.insertAt({} as InfraStudentApi.StudentContact, -1);
}
/** 删除学生联系人 */
@@ -59,26 +45,96 @@ const onDelete = async (row: InfraStudentApi.StudentContact) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): InfraStudentApi.StudentContact[] => {
const data = gridApi.grid.getData() as InfraStudentApi.StudentContact[];
const removeRecords = gridApi.grid.getRemoveRecords() as InfraStudentApi.StudentContact[];
const insertRecords = gridApi.grid.getInsertRecords() as InfraStudentApi.StudentContact[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): InfraStudentApi.StudentContact[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await getStudentContactListByStudentId(props.studentId!));
},
{ immediate: true },
@@ -88,13 +144,13 @@ watch(
<template>
<Grid class="mx-4">
<template #name="{ row }">
<el-input v-model="row.name" />
<ElInput v-model="row.name" />
</template>
<template #description="{ row }">
<el-input v-model="row.description" type="textarea" />
<ElInput v-model="row.description" type="textarea" />
</template>
<template #birthday="{ row }">
<el-date-picker
<ElDatePicker
v-model="row.birthday"
type="datetime"
value-format="x"
@@ -102,16 +158,16 @@ watch(
/>
</template>
<template #sex="{ row, column }">
<el-select v-model="row.sex" class="w-full">
<el-option v-for="option in column.params.options" :key="option.value" :value="option.value" :label="option.label" />
</el-select>
<ElSelect v-model="row.sex" class="w-full">
<ElOption v-for="option in column.params.options" :key="option.value" :value="option.value" :label="option.label" />
</ElSelect>
</template>
<template #enabled="{ row, column }">
<el-radio-group v-model="row.enabled">
<el-radio v-for="option in column.params.options" :key="option.value" :label="option.value">
<ElRadioGroup v-model="row.enabled">
<ElRadio v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</el-radio>
</el-radio-group>
</ElRadio>
</ElRadioGroup>
</template>
<template #avatar="{ row }">
<ImageUpload v-model="row.avatar" />
@@ -120,12 +176,25 @@ watch(
<FileUpload v-model="row.video" />
</template>
<template #memo="{ row }">
<el-input v-model="row.memo" type="textarea" />
<ElInput v-model="row.memo" type="textarea" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'danger', link: true,
icon: ACTION_ICON.DELETE,
auth: ['infra:student:delete'],
onClick: onDelete.bind(null, row),
},
]"
/>
</template>
</Grid>
<div class="flex justify-center -mt-4">
<el-button :icon="Plus" type="primary" plain @click="onAdd" v-access:code="['infra:student:create']">
<ElButton :icon="Plus" type="primary" plain @click="onAdd" v-access:code="['infra:student:create']">
{{ $t('ui.actionTitle.create', ['学生联系人']) }}
</el-button>
</ElButton>
</div>
</template>
@@ -39,10 +39,11 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await getStudentTeacherByStudentId(props.studentId!));
},
{ immediate: true },
@@ -42,7 +42,7 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -65,8 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -269,7 +268,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
minWidth: 120,
slots: { default: 'sex' },
params: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'boolean'),
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number'),
},
},
{
@@ -345,7 +344,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -368,8 +367,7 @@ export function useGridColumns(): VxeTableGridOptions<InfraStudentApi.Student>['
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { ElMessage, ElTabPane, ElTabs } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import { createStudent, getStudent, updateStudent } from '#/api/infra/demo';
@@ -48,7 +48,12 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 校验子表单
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
const studentContactValid = await studentContactFormRef.value?.validate();
if (!studentContactValid) {
subTabsName.value = 'studentContact';
return;
}
const studentTeacherValid = await studentTeacherFormRef.value?.validate();
if (!studentTeacherValid) {
subTabsName.value = 'studentTeacher';
return;
@@ -6,10 +6,9 @@
import { $t } from '#/locales';
import { Plus } from "@vben/icons";
import { ElButton, ElTabs, ElTabPane, ElCheckbox, ElInput, ElSelect, ElOption, ElRadioGroup, ElCheckboxGroup, ElDatePicker } from 'element-plus';
import { ElButton, ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElMessage, ElOption, ElRadio, ElRadioGroup, ElSelect, ElTabPane, ElTabs } from 'element-plus';
import { ImageUpload, FileUpload } from "#/components/upload";
import type { OnActionClickParams } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { useStudentContactGridEditColumns } from '../data';
import { getStudentContactListByStudentId } from '#/api/infra/demo';
@@ -17,22 +16,9 @@ const props = defineProps<{
studentId?: number // 学生编号(主表的关联字段)
}>()
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<InfraStudentApi.StudentContact>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useStudentContactGridEditColumns(onActionClick),
columns: useStudentContactGridEditColumns(),
border: true,
showOverflow: true,
autoResize: true,
@@ -51,7 +37,7 @@ gridOptions: {
/** 添加学生联系人 */
const onAdd = async () => {
await gridApi.grid.insertAt({} as StudentApi.StudentContact, -1);
await gridApi.grid.insertAt({} as InfraStudentApi.StudentContact, -1);
}
/** 删除学生联系人 */
@@ -59,26 +45,96 @@ const onDelete = async (row: InfraStudentApi.StudentContact) => {
await gridApi.grid.remove(row);
}
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): InfraStudentApi.StudentContact[] => {
const data = gridApi.grid.getData() as InfraStudentApi.StudentContact[];
const removeRecords = gridApi.grid.getRemoveRecords() as InfraStudentApi.StudentContact[];
const insertRecords = gridApi.grid.getInsertRecords() as InfraStudentApi.StudentContact[];
return data
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
},
});
/** 获取当前子表数据,新增行不提交表格生成的临时编号 */
function getData(): InfraStudentApi.StudentContact[] {
const { fullData } = gridApi.grid.getTableData();
const inserted = new Set(gridApi.grid.getInsertRecords());
return fullData.map((row) =>
inserted.has(row) ? { ...row, id: undefined } : { ...row },
);
}
/** 校验子表必填字段 */
function validate(): boolean {
for (const [index, row] of getData().entries()) {
if (
row.name === null ||
row.name === undefined ||
(typeof row.name === 'string' && String(row.name).trim() === '') ||
(Array.isArray(row.name) && row.name.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:名字不能为空`);
return false;
}
if (
row.description === null ||
row.description === undefined ||
(typeof row.description === 'string' && String(row.description).trim() === '') ||
(Array.isArray(row.description) && row.description.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:简介不能为空`);
return false;
}
if (
row.birthday === null ||
row.birthday === undefined ||
(typeof row.birthday === 'string' && String(row.birthday).trim() === '') ||
(Array.isArray(row.birthday) && row.birthday.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:出生日期不能为空`);
return false;
}
if (
row.sex === null ||
row.sex === undefined ||
(typeof row.sex === 'string' && String(row.sex).trim() === '') ||
(Array.isArray(row.sex) && row.sex.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:性别不能为空`);
return false;
}
if (
row.enabled === null ||
row.enabled === undefined ||
(typeof row.enabled === 'string' && String(row.enabled).trim() === '') ||
(Array.isArray(row.enabled) && row.enabled.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:是否有效不能为空`);
return false;
}
if (
row.avatar === null ||
row.avatar === undefined ||
(typeof row.avatar === 'string' && String(row.avatar).trim() === '') ||
(Array.isArray(row.avatar) && row.avatar.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:头像不能为空`);
return false;
}
if (
row.memo === null ||
row.memo === undefined ||
(typeof row.memo === 'string' && String(row.memo).trim() === '') ||
(Array.isArray(row.memo) && row.memo.length === 0)
) {
ElMessage.error(`第 ${index + 1} 行:备注不能为空`);
return false;
}
}
return true;
}
defineExpose({ getData, validate });
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.studentId,
async (val) => {
await nextTick();
if (!val) {
await gridApi.grid.loadData([]);
return;
}
await nextTick();
await gridApi.grid.loadData(await getStudentContactListByStudentId(props.studentId!));
},
{ immediate: true },
@@ -88,13 +144,13 @@ watch(
<template>
<Grid class="mx-4">
<template #name="{ row }">
<el-input v-model="row.name" />
<ElInput v-model="row.name" />
</template>
<template #description="{ row }">
<el-input v-model="row.description" type="textarea" />
<ElInput v-model="row.description" type="textarea" />
</template>
<template #birthday="{ row }">
<el-date-picker
<ElDatePicker
v-model="row.birthday"
type="datetime"
value-format="x"
@@ -102,16 +158,16 @@ watch(
/>
</template>
<template #sex="{ row, column }">
<el-select v-model="row.sex" class="w-full">
<el-option v-for="option in column.params.options" :key="option.value" :value="option.value" :label="option.label" />
</el-select>
<ElSelect v-model="row.sex" class="w-full">
<ElOption v-for="option in column.params.options" :key="option.value" :value="option.value" :label="option.label" />
</ElSelect>
</template>
<template #enabled="{ row, column }">
<el-radio-group v-model="row.enabled">
<el-radio v-for="option in column.params.options" :key="option.value" :label="option.value">
<ElRadioGroup v-model="row.enabled">
<ElRadio v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</el-radio>
</el-radio-group>
</ElRadio>
</ElRadioGroup>
</template>
<template #avatar="{ row }">
<ImageUpload v-model="row.avatar" />
@@ -120,12 +176,25 @@ watch(
<FileUpload v-model="row.video" />
</template>
<template #memo="{ row }">
<el-input v-model="row.memo" type="textarea" />
<ElInput v-model="row.memo" type="textarea" />
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.delete'),
type: 'danger', link: true,
icon: ACTION_ICON.DELETE,
auth: ['infra:student:delete'],
onClick: onDelete.bind(null, row),
},
]"
/>
</template>
</Grid>
<div class="flex justify-center -mt-4">
<el-button :icon="Plus" type="primary" plain @click="onAdd" v-access:code="['infra:student:create']">
<ElButton :icon="Plus" type="primary" plain @click="onAdd" v-access:code="['infra:student:create']">
{{ $t('ui.actionTitle.create', ['学生联系人']) }}
</el-button>
</ElButton>
</div>
</template>
@@ -39,10 +39,11 @@ defineExpose({
watch(
() => props.studentId,
async (val) => {
await nextTick();
await formApi.resetForm();
if (!val) {
return;
}
await nextTick();
await formApi.setValues(await getStudentTeacherByStudentId(props.studentId!));
},
{ immediate: true },
@@ -42,7 +42,7 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -65,8 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -42,7 +42,7 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
component: 'DatePicker',
componentProps: {
showTime: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
class: '!w-full',
@@ -65,8 +65,7 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
buttonStyle: 'solid',
optionType: 'button',
isButton: true,
},
},
{
@@ -50,6 +50,16 @@ function handleChange(file: any) {
}
}
/** 移除文件 */
function handleRemove() {
formApi.setFieldValue('file', undefined);
}
/** 超出文件数量 */
function handleExceed() {
ElMessage.warning('只能选择一个文件,请先移除已选文件');
}
/** 下载模板 */
async function handleDownload() {
const data = await importStudentTemplate();
@@ -66,6 +76,8 @@ async function handleDownload() {
:limit="1"
accept=".xls,.xlsx"
:on-change="handleChange"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:auto-upload="false"
>
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
@@ -32,13 +32,14 @@ export function useFormSchema(): VbenFormSchema[] {
id: 0,
name: '顶级分类',
});
return handleTree(data);
return handleTree(data, 'id', 'parentId');
},
labelField: 'name',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择上级分类',
treeDefaultExpandAll: true,
defaultExpandAll: true,
checkStrictly: true,
},
rules: 'selectRequired',
},
@@ -64,7 +64,7 @@
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
<Pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<StudentContactForm ref="formRef" @success="getList" />
@@ -79,9 +79,13 @@
components: {
StudentContactForm
},
props:[
'studentId'
],// 学生编号(主表的关联字段)
props: {
// 学生编号(主表的关联字段)
studentId: {
type: Number,
default: undefined
}
},
data() {
return {
// 遮罩层
@@ -64,7 +64,7 @@
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
<Pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<StudentTeacherForm ref="formRef" @success="getList" />
@@ -79,9 +79,13 @@
components: {
StudentTeacherForm
},
props:[
'studentId'
],// 学生编号(主表的关联字段)
props: {
// 学生编号(主表的关联字段)
studentId: {
type: Number,
default: undefined
}
},
data() {
return {
// 遮罩层
@@ -53,7 +53,7 @@
批量删除
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<RightToolbar :showSearch.sync="showSearch" @queryTable="getList"></RightToolbar>
</el-row>
<el-table
@@ -127,7 +127,7 @@
</el-table-column>
</el-table>
<!-- 分页组件 -->
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
<Pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
@pagination="getList"/>
<!-- 对话框(添加 / 修改) -->
<StudentForm ref="formRef" @success="getList" />
@@ -97,9 +97,13 @@
FileUpload,
Editor,
},
props:[
'studentId'
],// 学生编号(主表的关联字段)
props: {
// 学生编号(主表的关联字段)
studentId: {
type: Number,
default: undefined
}
},
data() {
return {
// 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -48,9 +48,13 @@
import * as StudentApi from '@/api/infra/demo';
export default {
name: "StudentContactList",
props:[
'studentId'
],// 学生编号(主表的关联字段)
props: {
// 学生编号(主表的关联字段)
studentId: {
type: Number,
default: undefined
}
},
data() {
return {
// 遮罩层

Some files were not shown because too many files have changed in this diff Show More