From 4e413a10e621a34235a6f520f8b97cd77cc00c95 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 9 Nov 2023 23:10:54 +0800
Subject: [PATCH 01/15] =?UTF-8?q?=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=9A?=
 =?UTF-8?q?=E6=9A=82=E6=97=B6=E5=AD=98=E5=82=A8=EF=BC=8C=E5=87=86=E5=A4=87?=
 =?UTF-8?q?=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo02/index.ts                 |  35 ++++
 .../infra/demo02/DemoStudentContactForm.vue   |  71 ++++++++
 src/views/infra/demo02/DemoStudentForm.vue    |  97 +++++++++++
 src/views/infra/demo02/index.vue              | 159 ++++++++++++++++++
 4 files changed, 362 insertions(+)
 create mode 100644 src/api/infra/demo02/index.ts
 create mode 100644 src/views/infra/demo02/DemoStudentContactForm.vue
 create mode 100644 src/views/infra/demo02/DemoStudentForm.vue
 create mode 100644 src/views/infra/demo02/index.vue

diff --git a/src/api/infra/demo02/index.ts b/src/api/infra/demo02/index.ts
new file mode 100644
index 00000000..b575dba4
--- /dev/null
+++ b/src/api/infra/demo02/index.ts
@@ -0,0 +1,35 @@
+import request from '@/config/axios'
+
+export interface DemoStudentVO {
+  id: number
+}
+
+// 查询学生列表
+export const getDemoStudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemoStudent = async (id: number) => {
+  return await request.get({ url: `/infra/demo-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemoStudent = async (data: DemoStudentVO) => {
+  return await request.post({ url: `/infra/demo-student/create`, data })
+}
+
+// 修改学生
+export const updateDemoStudent = async (data: DemoStudentVO) => {
+  return await request.put({ url: `/infra/demo-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemoStudent = async (id: number) => {
+  return await request.delete({ url: `/infra/demo-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemoStudent = async (params) => {
+  return await request.download({ url: `/infra/demo-student/export-excel`, params })
+}
diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
new file mode 100644
index 00000000..98c97fab
--- /dev/null
+++ b/src/views/infra/demo02/DemoStudentContactForm.vue
@@ -0,0 +1,71 @@
+<template>
+  <!--  <el-row :gutter="10" class="mb2">-->
+  <!--    <el-col :span="1.5">-->
+  <!--      <el-button type="primary" @click="handleAdd">添加</el-button>-->
+  <!--    </el-col>-->
+  <!--    <el-col :span="1.5">-->
+  <!--      <el-button type="danger">删除</el-button>-->
+  <!--    </el-col>-->
+  <!--  </el-row>-->
+  <el-table
+    :data="formData"
+    @selection-change="handleDemoStudentContactSelectionChange"
+    ref="demoStudentContactRef"
+    :stripe="true"
+  >
+    <el-table-column label="序号" type="index" width="100" />
+    <el-table-column label="名字" prop="name" width="300">
+      <template #default="scope">
+        <el-form-item label-width="0px" :inline-message="true">
+          <el-input v-model="scope.row.name" placeholder="请输入名字" />
+        </el-form-item>
+      </template>
+    </el-table-column>
+    <el-table-column label="手机号码">
+      <template #default="{ row, $index }">
+        <el-form-item
+          label-width="0px"
+          :prop="`formData.${$index}.mobile`"
+          :rules="formRules.mobile"
+          :inline-message="true"
+        >
+          <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
+        </el-form-item>
+      </template>
+    </el-table-column>
+  </el-table>
+  <el-button @click="handleAdd" class="w-1/1">+ 添加客户信息</el-button>
+</template>
+<script setup lang="ts">
+const formData = ref([
+  {
+    name: '芋艿'
+  },
+  {
+    name: '土豆'
+  }
+])
+const formRules = reactive({
+  mobile: [required]
+})
+
+const handleDemoStudentContactSelectionChange = (val) => {
+  demoStudentContactList.value = val
+}
+
+const demoStudentContactRef = ref()
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  formData.value.push({
+    name: '测试'
+  })
+}
+
+/** 删除按钮操作 */
+const handleRemove = () => {
+  formData.value.push({
+    name: '测试'
+  })
+}
+</script>
diff --git a/src/views/infra/demo02/DemoStudentForm.vue b/src/views/infra/demo02/DemoStudentForm.vue
new file mode 100644
index 00000000..62303dc0
--- /dev/null
+++ b/src/views/infra/demo02/DemoStudentForm.vue
@@ -0,0 +1,97 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="表描述" prop="tableComment">
+        <el-input v-model="formData.tableComment" placeholder="请输入" />
+      </el-form-item>
+      <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
+        <el-tab-pane label="User" name="first">
+          <DemoStudentContactForm />
+        </el-tab-pane>
+        <el-tab-pane label="Config" name="second">Config</el-tab-pane>
+        <el-tab-pane label="Role" name="third">Role</el-tab-pane>
+        <el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
+      </el-tabs>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentContactForm from './DemoStudentContactForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  demoStudentContactList: []
+})
+const formRules = reactive({})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  // resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await DemoStudentApi.getDemoStudent(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
+    if (formType.value === 'create') {
+      await DemoStudentApi.createDemoStudent(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DemoStudentApi.updateDemoStudent(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo02/index.vue b/src/views/infra/demo02/index.vue
new file mode 100644
index 00000000..ae46f8b4
--- /dev/null
+++ b/src/views/infra/demo02/index.vue
@@ -0,0 +1,159 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo-student:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.id" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <DemoStudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import download from '@/utils/download'
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentForm from './DemoStudentForm.vue'
+
+defineOptions({ name: 'InfraDemoStudent' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DemoStudentApi.deleteDemoStudent(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await DemoStudentApi.exportDemoStudent(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

From d45ebd4fcfb0bac96a575b5f594248e5b85f8778 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 10 Nov 2023 00:20:00 +0800
Subject: [PATCH 02/15] =?UTF-8?q?=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=9A?=
 =?UTF-8?q?=E6=9A=82=E6=97=B6=E5=AD=98=E5=82=A8=EF=BC=8C=E6=A0=B7=E5=BC=8F?=
 =?UTF-8?q?=E8=B7=91=E9=80=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../infra/demo02/DemoStudentContactForm.vue   | 54 +++++++++++--------
 src/views/infra/demo02/DemoStudentForm.vue    | 26 ++++++---
 2 files changed, 49 insertions(+), 31 deletions(-)

diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
index 98c97fab..145b4889 100644
--- a/src/views/infra/demo02/DemoStudentContactForm.vue
+++ b/src/views/infra/demo02/DemoStudentContactForm.vue
@@ -1,22 +1,15 @@
 <template>
-  <!--  <el-row :gutter="10" class="mb2">-->
-  <!--    <el-col :span="1.5">-->
-  <!--      <el-button type="primary" @click="handleAdd">添加</el-button>-->
-  <!--    </el-col>-->
-  <!--    <el-col :span="1.5">-->
-  <!--      <el-button type="danger">删除</el-button>-->
-  <!--    </el-col>-->
-  <!--  </el-row>-->
   <el-table
     :data="formData"
     @selection-change="handleDemoStudentContactSelectionChange"
     ref="demoStudentContactRef"
     :stripe="true"
+    class="-mt-10px"
   >
     <el-table-column label="序号" type="index" width="100" />
     <el-table-column label="名字" prop="name" width="300">
       <template #default="scope">
-        <el-form-item label-width="0px" :inline-message="true">
+        <el-form-item label-width="0px" :inline-message="true" class="mb-0px!">
           <el-input v-model="scope.row.name" placeholder="请输入名字" />
         </el-form-item>
       </template>
@@ -25,26 +18,37 @@
       <template #default="{ row, $index }">
         <el-form-item
           label-width="0px"
-          :prop="`formData.${$index}.mobile`"
+          :prop="`demoStudentContactList.${$index}.mobile`"
           :rules="formRules.mobile"
           :inline-message="true"
+          class="mb-0px!"
         >
           <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
         </el-form-item>
       </template>
     </el-table-column>
+    <el-table-column align="center" fixed="right" label="操作" width="60">
+      <el-button @click="handleAdd" link>—</el-button>
+    </el-table-column>
   </el-table>
-  <el-button @click="handleAdd" class="w-1/1">+ 添加客户信息</el-button>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加联系人</el-button>
+  </el-row>
 </template>
 <script setup lang="ts">
-const formData = ref([
-  {
-    name: '芋艿'
-  },
-  {
-    name: '土豆'
-  }
-])
+const props = defineProps<{
+  formData: any[]
+}>()
+// const formData = ref([
+//   {
+//     name: '芋艿',
+//     mobile: '15601691300'
+//   },
+//   {
+//     name: '土豆',
+//     mobile: '15601691234'
+//   }
+// ])
 const formRules = reactive({
   mobile: [required]
 })
@@ -56,15 +60,19 @@ const handleDemoStudentContactSelectionChange = (val) => {
 const demoStudentContactRef = ref()
 
 /** 新增按钮操作 */
+const emit = defineEmits(['update:formData'])
 const handleAdd = () => {
-  formData.value.push({
-    name: '测试'
-  })
+  emit('update:formData', [
+    ...props.formData,
+    {
+      name: '土豆'
+    }
+  ])
 }
 
 /** 删除按钮操作 */
 const handleRemove = () => {
-  formData.value.push({
+  formData.push({
     name: '测试'
   })
 }
diff --git a/src/views/infra/demo02/DemoStudentForm.vue b/src/views/infra/demo02/DemoStudentForm.vue
index 62303dc0..884788dc 100644
--- a/src/views/infra/demo02/DemoStudentForm.vue
+++ b/src/views/infra/demo02/DemoStudentForm.vue
@@ -7,16 +7,21 @@
       label-width="100px"
       v-loading="formLoading"
     >
-      <el-form-item label="表描述" prop="tableComment">
-        <el-input v-model="formData.tableComment" placeholder="请输入" />
+      <el-form-item label="字段 1" prop="field1">
+        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+      </el-form-item>
+      <el-form-item label="字段 2" prop="field2">
+        <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+      </el-form-item>
+      <el-form-item label="字段 3" prop="field3">
+        <el-input v-model="formData.field3" placeholder="请输入字段 3" />
       </el-form-item>
       <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
-        <el-tab-pane label="User" name="first">
-          <DemoStudentContactForm />
+        <el-tab-pane label="联系人信息" name="first">
+          <DemoStudentContactForm v-model:form-data="formData.demoStudentContactList" />
         </el-tab-pane>
-        <el-tab-pane label="Config" name="second">Config</el-tab-pane>
-        <el-tab-pane label="Role" name="third">Role</el-tab-pane>
-        <el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
+        <el-tab-pane label="地址信息" name="third">地址信息</el-tab-pane>
+        <el-tab-pane label="其它信息" name="fourth">其它信息</el-tab-pane>
       </el-tabs>
     </el-form>
     <template #footer>
@@ -38,7 +43,12 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
-  demoStudentContactList: []
+  demoStudentContactList: [
+    {
+      name: '芋艿',
+      mobile: '15601691300'
+    }
+  ]
 })
 const formRules = reactive({})
 const formRef = ref() // 表单 Ref

From 86d9a97ccb08bec766ada95d5860deb28588e2b4 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 10 Nov 2023 08:51:01 +0800
Subject: [PATCH 03/15] =?UTF-8?q?=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=9A?=
 =?UTF-8?q?=E6=9A=82=E6=97=B6=E5=AD=98=E5=82=A8=EF=BC=8C=E5=8F=82=E6=95=B0?=
 =?UTF-8?q?=E6=A0=A1=E9=AA=8C-=E8=A1=A8=E5=8D=95=E6=95=B0=E6=8D=AE?=
 =?UTF-8?q?=E8=B7=91=E9=80=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../infra/demo02/DemoStudentContactForm.vue   | 27 ++++++-------------
 src/views/infra/demo02/DemoStudentForm.vue    | 14 +++++-----
 2 files changed, 15 insertions(+), 26 deletions(-)

diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
index 145b4889..0cd9d106 100644
--- a/src/views/infra/demo02/DemoStudentContactForm.vue
+++ b/src/views/infra/demo02/DemoStudentContactForm.vue
@@ -1,12 +1,6 @@
 <template>
-  <el-table
-    :data="formData"
-    @selection-change="handleDemoStudentContactSelectionChange"
-    ref="demoStudentContactRef"
-    :stripe="true"
-    class="-mt-10px"
-  >
-    <el-table-column label="序号" type="index" width="100" />
+  <el-table :data="formData" :stripe="true" class="-mt-10px">
+    <el-table-column label="序号" type="index" width="60" />
     <el-table-column label="名字" prop="name" width="300">
       <template #default="scope">
         <el-form-item label-width="0px" :inline-message="true" class="mb-0px!">
@@ -28,7 +22,9 @@
       </template>
     </el-table-column>
     <el-table-column align="center" fixed="right" label="操作" width="60">
-      <el-button @click="handleAdd" link>—</el-button>
+      <template #default="{ $index }">
+        <el-button @click="handleRemove($index)" link>—</el-button>
+      </template>
     </el-table-column>
   </el-table>
   <el-row justify="center" class="mt-3">
@@ -53,12 +49,6 @@ const formRules = reactive({
   mobile: [required]
 })
 
-const handleDemoStudentContactSelectionChange = (val) => {
-  demoStudentContactList.value = val
-}
-
-const demoStudentContactRef = ref()
-
 /** 新增按钮操作 */
 const emit = defineEmits(['update:formData'])
 const handleAdd = () => {
@@ -71,9 +61,8 @@ const handleAdd = () => {
 }
 
 /** 删除按钮操作 */
-const handleRemove = () => {
-  formData.push({
-    name: '测试'
-  })
+const handleRemove = (index) => {
+  const formData = props.formData.filter((_, i) => i !== index)
+  emit('update:formData', formData)
 }
 </script>
diff --git a/src/views/infra/demo02/DemoStudentForm.vue b/src/views/infra/demo02/DemoStudentForm.vue
index 884788dc..1bb6d7ea 100644
--- a/src/views/infra/demo02/DemoStudentForm.vue
+++ b/src/views/infra/demo02/DemoStudentForm.vue
@@ -16,14 +16,14 @@
       <el-form-item label="字段 3" prop="field3">
         <el-input v-model="formData.field3" placeholder="请输入字段 3" />
       </el-form-item>
-      <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
-        <el-tab-pane label="联系人信息" name="first">
-          <DemoStudentContactForm v-model:form-data="formData.demoStudentContactList" />
-        </el-tab-pane>
-        <el-tab-pane label="地址信息" name="third">地址信息</el-tab-pane>
-        <el-tab-pane label="其它信息" name="fourth">其它信息</el-tab-pane>
-      </el-tabs>
     </el-form>
+    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
+      <el-tab-pane label="联系人信息" name="first">
+        <DemoStudentContactForm v-model:form-data="formData.demoStudentContactList" />
+      </el-tab-pane>
+      <el-tab-pane label="地址信息" name="third">地址信息</el-tab-pane>
+      <el-tab-pane label="其它信息" name="fourth">其它信息</el-tab-pane>
+    </el-tabs>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>

From 9f9e0f8bda0c19afbd6bc1d1290062d8f5ed9d3c Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 10 Nov 2023 19:54:55 +0800
Subject: [PATCH 04/15] =?UTF-8?q?=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=9A?=
 =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=89=E7=A7=8D=E6=A8=A1=E5=BC=8F=E7=9A=84?=
 =?UTF-8?q?=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../infra/demo02/DemoStudentAddressForm.vue   |  58 ++++++
 .../infra/demo02/DemoStudentContactForm.vue   | 119 ++++++-----
 src/views/infra/demo02/DemoStudentForm.vue    |  61 ++++--
 src/views/infra/demo02/index.vue              |  33 ++--
 .../infra/demo03/DemoStudentAddressForm.vue   |  58 ++++++
 .../infra/demo03/DemoStudentAddressList.vue   |  38 ++++
 .../infra/demo03/DemoStudentContactForm.vue   |  89 +++++++++
 .../infra/demo03/DemoStudentContactList.vue   |  38 ++++
 src/views/infra/demo03/DemoStudentForm.vue    | 132 +++++++++++++
 src/views/infra/demo03/index.vue              | 176 +++++++++++++++++
 .../infra/demo04/DemoStudentAddressForm.vue   |  58 ++++++
 .../infra/demo04/DemoStudentAddressList.vue   |  39 ++++
 .../infra/demo04/DemoStudentContactForm.vue   |  96 +++++++++
 .../infra/demo04/DemoStudentContactList.vue   |  70 +++++++
 src/views/infra/demo04/DemoStudentForm.vue    | 132 +++++++++++++
 src/views/infra/demo04/index.vue              | 184 ++++++++++++++++++
 16 files changed, 1294 insertions(+), 87 deletions(-)
 create mode 100644 src/views/infra/demo02/DemoStudentAddressForm.vue
 create mode 100644 src/views/infra/demo03/DemoStudentAddressForm.vue
 create mode 100644 src/views/infra/demo03/DemoStudentAddressList.vue
 create mode 100644 src/views/infra/demo03/DemoStudentContactForm.vue
 create mode 100644 src/views/infra/demo03/DemoStudentContactList.vue
 create mode 100644 src/views/infra/demo03/DemoStudentForm.vue
 create mode 100644 src/views/infra/demo03/index.vue
 create mode 100644 src/views/infra/demo04/DemoStudentAddressForm.vue
 create mode 100644 src/views/infra/demo04/DemoStudentAddressList.vue
 create mode 100644 src/views/infra/demo04/DemoStudentContactForm.vue
 create mode 100644 src/views/infra/demo04/DemoStudentContactList.vue
 create mode 100644 src/views/infra/demo04/DemoStudentForm.vue
 create mode 100644 src/views/infra/demo04/index.vue

diff --git a/src/views/infra/demo02/DemoStudentAddressForm.vue b/src/views/infra/demo02/DemoStudentAddressForm.vue
new file mode 100644
index 00000000..af4cd8f1
--- /dev/null
+++ b/src/views/infra/demo02/DemoStudentAddressForm.vue
@@ -0,0 +1,58 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+    <el-form-item label="子字段 1" prop="field1">
+      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+    </el-form-item>
+    <el-form-item label="子字段 2" prop="field2">
+      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+    </el-form-item>
+    <el-form-item label="子字段 3" prop="field3">
+      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref({})
+const formRules = reactive({
+  field1: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    if (val) {
+      formData.value = {
+        field2: '番茄',
+        field3: '西瓜'
+      }
+    } else {
+      formData.value = {}
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 **/
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
index 0cd9d106..50c44263 100644
--- a/src/views/infra/demo02/DemoStudentContactForm.vue
+++ b/src/views/infra/demo02/DemoStudentContactForm.vue
@@ -1,68 +1,89 @@
 <template>
-  <el-table :data="formData" :stripe="true" class="-mt-10px">
-    <el-table-column label="序号" type="index" width="60" />
-    <el-table-column label="名字" prop="name" width="300">
-      <template #default="scope">
-        <el-form-item label-width="0px" :inline-message="true" class="mb-0px!">
-          <el-input v-model="scope.row.name" placeholder="请输入名字" />
-        </el-form-item>
-      </template>
-    </el-table-column>
-    <el-table-column label="手机号码">
-      <template #default="{ row, $index }">
-        <el-form-item
-          label-width="0px"
-          :prop="`demoStudentContactList.${$index}.mobile`"
-          :rules="formRules.mobile"
-          :inline-message="true"
-          class="mb-0px!"
-        >
-          <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
-        </el-form-item>
-      </template>
-    </el-table-column>
-    <el-table-column align="center" fixed="right" label="操作" width="60">
-      <template #default="{ $index }">
-        <el-button @click="handleRemove($index)" link>—</el-button>
-      </template>
-    </el-table-column>
-  </el-table>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="0px"
+    v-loading="formLoading"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+      <el-table-column label="名字" prop="name" width="300">
+        <template #default="row">
+          <el-form-item class="mb-0px!">
+            <el-input v-model="row.name" placeholder="请输入名字" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号码">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.mobile`" :rules="formRules.mobile" class="mb-0px!">
+            <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
   <el-row justify="center" class="mt-3">
     <el-button @click="handleAdd" round>+ 添加联系人</el-button>
   </el-row>
 </template>
 <script setup lang="ts">
 const props = defineProps<{
-  formData: any[]
+  studentId: undefined // 学生编号
 }>()
-// const formData = ref([
-//   {
-//     name: '芋艿',
-//     mobile: '15601691300'
-//   },
-//   {
-//     name: '土豆',
-//     mobile: '15601691234'
-//   }
-// ])
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
 const formRules = reactive({
   mobile: [required]
 })
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    if (val) {
+      formData.value = [
+        {
+          name: '芋艿',
+          mobile: '15601691300'
+        }
+      ]
+    } else {
+      formData.value = []
+    }
+  },
+  { immediate: true }
+)
 
 /** 新增按钮操作 */
-const emit = defineEmits(['update:formData'])
 const handleAdd = () => {
-  emit('update:formData', [
-    ...props.formData,
-    {
-      name: '土豆'
-    }
-  ])
+  formData.value.push({
+    name: '土豆'
+  })
 }
 
 /** 删除按钮操作 */
-const handleRemove = (index) => {
-  const formData = props.formData.filter((_, i) => i !== index)
-  emit('update:formData', formData)
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
 }
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 **/
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
 </script>
diff --git a/src/views/infra/demo02/DemoStudentForm.vue b/src/views/infra/demo02/DemoStudentForm.vue
index 1bb6d7ea..fe0788c4 100644
--- a/src/views/infra/demo02/DemoStudentForm.vue
+++ b/src/views/infra/demo02/DemoStudentForm.vue
@@ -17,12 +17,14 @@
         <el-input v-model="formData.field3" placeholder="请输入字段 3" />
       </el-form-item>
     </el-form>
-    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
-      <el-tab-pane label="联系人信息" name="first">
-        <DemoStudentContactForm v-model:form-data="formData.demoStudentContactList" />
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="联系人信息" name="DemoStudentContact">
+        <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="地址信息" name="DemoStudentAddress">
+        <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
       </el-tab-pane>
-      <el-tab-pane label="地址信息" name="third">地址信息</el-tab-pane>
-      <el-tab-pane label="其它信息" name="fourth">其它信息</el-tab-pane>
     </el-tabs>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
@@ -33,6 +35,7 @@
 <script setup lang="ts">
 import * as DemoStudentApi from '@/api/infra/demo02'
 import DemoStudentContactForm from './DemoStudentContactForm.vue'
+import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -42,28 +45,36 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
-  id: undefined,
-  demoStudentContactList: [
-    {
-      name: '芋艿',
-      mobile: '15601691300'
-    }
-  ]
+  id: undefined
+})
+const formRules = reactive({
+  field2: [required]
 })
-const formRules = reactive({})
 const formRef = ref() // 表单 Ref
 
+/** 子表的表单 */
+const demoStudentContactFormRef = ref()
+const demoStudentAddressFormRef = ref()
+const subTabsName = ref('DemoStudentContact')
+
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
-  // resetForm()
+  resetForm()
   // 修改时,设置数据
   if (id) {
+    // debugger
     formLoading.value = true
     try {
-      formData.value = await DemoStudentApi.getDemoStudent(id)
+      // formData.value = await DemoStudentApi.getDemoStudent(id)
+      formData.value = {
+        id: id,
+        field1: '1',
+        field2: '22',
+        field3: '333'
+      }
     } finally {
       formLoading.value = false
     }
@@ -75,13 +86,27 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
   // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demoStudentContactFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'DemoStudentContact'
+    return
+  }
+  try {
+    await demoStudentAddressFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'DemoStudentAddress'
+    return
+  }
   // 提交请求
   formLoading.value = true
   try {
     const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
+    // 拼接子表的数据
+    data.demoStudentContacts = demoStudentContactFormRef.value.getData()
+    data.demoStudentAddress = demoStudentAddressFormRef.value.getData()
     if (formType.value === 'create') {
       await DemoStudentApi.createDemoStudent(data)
       message.success(t('common.createSuccess'))
diff --git a/src/views/infra/demo02/index.vue b/src/views/infra/demo02/index.vue
index ae46f8b4..89fd656b 100644
--- a/src/views/infra/demo02/index.vue
+++ b/src/views/infra/demo02/index.vue
@@ -11,12 +11,7 @@
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['infra:demo-student:create']"
-        >
+        <el-button type="primary" plain @click="openForm('create')">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
@@ -35,19 +30,10 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.id" />
-        </template>
-      </el-table-column>
+      <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="操作" align="center">
         <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['infra:demo-student:update']"
-          >
+          <el-button link type="primary" @click="openForm('update', scope.row.id)">
             编辑
           </el-button>
           <el-button
@@ -98,9 +84,13 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = data.list
-    total.value = data.total
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: 1
+      }
+    ]
+    total.value = 10
   } finally {
     loading.value = false
   }
@@ -120,7 +110,10 @@ const resetQuery = () => {
 
 /** 添加/修改操作 */
 const formRef = ref()
+// const demoStudentContactFormRef = ref()
 const openForm = (type: string, id?: number) => {
+  // console.log(demoStudentContactFormRef, 'xx demoStudentContactFormRef xx')
+  // demoStudentContactFormRef.value.validate()
   formRef.value.open(type, id)
 }
 
diff --git a/src/views/infra/demo03/DemoStudentAddressForm.vue b/src/views/infra/demo03/DemoStudentAddressForm.vue
new file mode 100644
index 00000000..af4cd8f1
--- /dev/null
+++ b/src/views/infra/demo03/DemoStudentAddressForm.vue
@@ -0,0 +1,58 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+    <el-form-item label="子字段 1" prop="field1">
+      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+    </el-form-item>
+    <el-form-item label="子字段 2" prop="field2">
+      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+    </el-form-item>
+    <el-form-item label="子字段 3" prop="field3">
+      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref({})
+const formRules = reactive({
+  field1: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    if (val) {
+      formData.value = {
+        field2: '番茄',
+        field3: '西瓜'
+      }
+    } else {
+      formData.value = {}
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 **/
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo03/DemoStudentAddressList.vue b/src/views/infra/demo03/DemoStudentAddressList.vue
new file mode 100644
index 00000000..2354db56
--- /dev/null
+++ b/src/views/infra/demo03/DemoStudentAddressList.vue
@@ -0,0 +1,38 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="手机" align="center" prop="mobile" />
+    </el-table>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: props.studentId,
+        mobile: '88888'
+      }
+    ]
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
diff --git a/src/views/infra/demo03/DemoStudentContactForm.vue b/src/views/infra/demo03/DemoStudentContactForm.vue
new file mode 100644
index 00000000..50c44263
--- /dev/null
+++ b/src/views/infra/demo03/DemoStudentContactForm.vue
@@ -0,0 +1,89 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="0px"
+    v-loading="formLoading"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+      <el-table-column label="名字" prop="name" width="300">
+        <template #default="row">
+          <el-form-item class="mb-0px!">
+            <el-input v-model="row.name" placeholder="请输入名字" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="手机号码">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.mobile`" :rules="formRules.mobile" class="mb-0px!">
+            <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加联系人</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  mobile: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    if (val) {
+      formData.value = [
+        {
+          name: '芋艿',
+          mobile: '15601691300'
+        }
+      ]
+    } else {
+      formData.value = []
+    }
+  },
+  { immediate: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  formData.value.push({
+    name: '土豆'
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 **/
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo03/DemoStudentContactList.vue b/src/views/infra/demo03/DemoStudentContactList.vue
new file mode 100644
index 00000000..76d46116
--- /dev/null
+++ b/src/views/infra/demo03/DemoStudentContactList.vue
@@ -0,0 +1,38 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="手机" align="center" prop="mobile" />
+    </el-table>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: props.studentId,
+        mobile: '15601691300'
+      }
+    ]
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
diff --git a/src/views/infra/demo03/DemoStudentForm.vue b/src/views/infra/demo03/DemoStudentForm.vue
new file mode 100644
index 00000000..fe0788c4
--- /dev/null
+++ b/src/views/infra/demo03/DemoStudentForm.vue
@@ -0,0 +1,132 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="字段 1" prop="field1">
+        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+      </el-form-item>
+      <el-form-item label="字段 2" prop="field2">
+        <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+      </el-form-item>
+      <el-form-item label="字段 3" prop="field3">
+        <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+      </el-form-item>
+    </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="联系人信息" name="DemoStudentContact">
+        <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="地址信息" name="DemoStudentAddress">
+        <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentContactForm from './DemoStudentContactForm.vue'
+import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined
+})
+const formRules = reactive({
+  field2: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 子表的表单 */
+const demoStudentContactFormRef = ref()
+const demoStudentAddressFormRef = ref()
+const subTabsName = ref('DemoStudentContact')
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    // debugger
+    formLoading.value = true
+    try {
+      // formData.value = await DemoStudentApi.getDemoStudent(id)
+      formData.value = {
+        id: id,
+        field1: '1',
+        field2: '22',
+        field3: '333'
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demoStudentContactFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'DemoStudentContact'
+    return
+  }
+  try {
+    await demoStudentAddressFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'DemoStudentAddress'
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
+    // 拼接子表的数据
+    data.demoStudentContacts = demoStudentContactFormRef.value.getData()
+    data.demoStudentAddress = demoStudentAddressFormRef.value.getData()
+    if (formType.value === 'create') {
+      await DemoStudentApi.createDemoStudent(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DemoStudentApi.updateDemoStudent(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo03/index.vue b/src/views/infra/demo03/index.vue
new file mode 100644
index 00000000..afd5cbfc
--- /dev/null
+++ b/src/views/infra/demo03/index.vue
@@ -0,0 +1,176 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" plain @click="openForm('create')">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      row-key="id"
+    >
+      <el-table-column type="expand">
+        <template #default="scope">
+          <!-- 子表的表单 -->
+          <el-tabs model-value="DemoStudentContact">
+            <el-tab-pane label="联系人信息" name="DemoStudentContact">
+              <DemoStudentContactList :student-id="scope.row.id" />
+            </el-tab-pane>
+            <el-tab-pane label="地址信息" name="DemoStudentAddress">
+              <DemoStudentAddressList :student-id="scope.row.id" />
+            </el-tab-pane>
+          </el-tabs>
+        </template>
+      </el-table-column>
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button link type="primary" @click="openForm('update', scope.row.id)">
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <DemoStudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import download from '@/utils/download'
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentForm from './DemoStudentForm.vue'
+import DemoStudentContactList from './DemoStudentContactList.vue'
+import DemoStudentAddressList from './DemoStudentAddressList.vue'
+
+defineOptions({ name: 'InfraDemoStudent' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: 1
+      },
+      {
+        id: 10
+      }
+    ]
+    total.value = 10
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+// const demoStudentContactFormRef = ref()
+const openForm = (type: string, id?: number) => {
+  // console.log(demoStudentContactFormRef, 'xx demoStudentContactFormRef xx')
+  // demoStudentContactFormRef.value.validate()
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DemoStudentApi.deleteDemoStudent(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await DemoStudentApi.exportDemoStudent(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
diff --git a/src/views/infra/demo04/DemoStudentAddressForm.vue b/src/views/infra/demo04/DemoStudentAddressForm.vue
new file mode 100644
index 00000000..af4cd8f1
--- /dev/null
+++ b/src/views/infra/demo04/DemoStudentAddressForm.vue
@@ -0,0 +1,58 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+    <el-form-item label="子字段 1" prop="field1">
+      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+    </el-form-item>
+    <el-form-item label="子字段 2" prop="field2">
+      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+    </el-form-item>
+    <el-form-item label="子字段 3" prop="field3">
+      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref({})
+const formRules = reactive({
+  field1: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    if (val) {
+      formData.value = {
+        field2: '番茄',
+        field3: '西瓜'
+      }
+    } else {
+      formData.value = {}
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 **/
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo04/DemoStudentAddressList.vue b/src/views/infra/demo04/DemoStudentAddressList.vue
new file mode 100644
index 00000000..92143456
--- /dev/null
+++ b/src/views/infra/demo04/DemoStudentAddressList.vue
@@ -0,0 +1,39 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="手机" align="center" prop="mobile" />
+    </el-table>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+// TODO 芋艿:暂时没改
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: props.studentId,
+        mobile: '88888'
+      }
+    ]
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
diff --git a/src/views/infra/demo04/DemoStudentContactForm.vue b/src/views/infra/demo04/DemoStudentContactForm.vue
new file mode 100644
index 00000000..c8744eb0
--- /dev/null
+++ b/src/views/infra/demo04/DemoStudentContactForm.vue
@@ -0,0 +1,96 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="字段 1" prop="field1">
+        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentContactForm from './DemoStudentContactForm.vue'
+import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined
+})
+const formRules = reactive({
+  field2: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    // debugger
+    formLoading.value = true
+    try {
+      // formData.value = await DemoStudentApi.getDemoStudent(id)
+      formData.value = {
+        id: id,
+        field1: '1',
+        field2: '22',
+        field3: '333'
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
+    if (formType.value === 'create') {
+      // await DemoStudentApi.createDemoStudent(data) // TODO 芋艿:临时去掉
+      message.success(t('common.createSuccess'))
+    } else {
+      await DemoStudentApi.updateDemoStudent(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo04/DemoStudentContactList.vue b/src/views/infra/demo04/DemoStudentContactList.vue
new file mode 100644
index 00000000..b944c407
--- /dev/null
+++ b/src/views/infra/demo04/DemoStudentContactList.vue
@@ -0,0 +1,70 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button type="primary" plain @click="openForm('create')">
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="手机" align="center" prop="mobile" />
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <DemoStudentContactForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import DemoStudentContactForm from './DemoStudentContactForm.vue'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: props.studentId,
+        mobile: '15601691300'
+      }
+    ]
+    total.value = 10
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+</script>
diff --git a/src/views/infra/demo04/DemoStudentForm.vue b/src/views/infra/demo04/DemoStudentForm.vue
new file mode 100644
index 00000000..fe0788c4
--- /dev/null
+++ b/src/views/infra/demo04/DemoStudentForm.vue
@@ -0,0 +1,132 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="字段 1" prop="field1">
+        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+      </el-form-item>
+      <el-form-item label="字段 2" prop="field2">
+        <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+      </el-form-item>
+      <el-form-item label="字段 3" prop="field3">
+        <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+      </el-form-item>
+    </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="联系人信息" name="DemoStudentContact">
+        <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="地址信息" name="DemoStudentAddress">
+        <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentContactForm from './DemoStudentContactForm.vue'
+import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined
+})
+const formRules = reactive({
+  field2: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 子表的表单 */
+const demoStudentContactFormRef = ref()
+const demoStudentAddressFormRef = ref()
+const subTabsName = ref('DemoStudentContact')
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    // debugger
+    formLoading.value = true
+    try {
+      // formData.value = await DemoStudentApi.getDemoStudent(id)
+      formData.value = {
+        id: id,
+        field1: '1',
+        field2: '22',
+        field3: '333'
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demoStudentContactFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'DemoStudentContact'
+    return
+  }
+  try {
+    await demoStudentAddressFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'DemoStudentAddress'
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
+    // 拼接子表的数据
+    data.demoStudentContacts = demoStudentContactFormRef.value.getData()
+    data.demoStudentAddress = demoStudentAddressFormRef.value.getData()
+    if (formType.value === 'create') {
+      await DemoStudentApi.createDemoStudent(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DemoStudentApi.updateDemoStudent(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo04/index.vue b/src/views/infra/demo04/index.vue
new file mode 100644
index 00000000..9a6b8bfe
--- /dev/null
+++ b/src/views/infra/demo04/index.vue
@@ -0,0 +1,184 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button type="primary" plain @click="openForm('create')">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      highlight-current-row
+      @current-change="handleCurrentChange"
+    >
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button link type="primary" @click="openForm('update', scope.row.id)">
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 子列表 -->
+  <ContentWrap>
+    <el-tabs model-value="DemoStudentContact">
+      <el-tab-pane label="联系人信息" name="DemoStudentContact">
+        <DemoStudentContactList :student-id="currentRow.id" />
+      </el-tab-pane>
+      <el-tab-pane label="地址信息" name="DemoStudentAddress">
+        <DemoStudentAddressList :student-id="currentRow.id" />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <DemoStudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import download from '@/utils/download'
+import * as DemoStudentApi from '@/api/infra/demo02'
+import DemoStudentForm from './DemoStudentForm.vue'
+import DemoStudentContactList from './DemoStudentContactList.vue'
+import DemoStudentAddressList from './DemoStudentAddressList.vue'
+
+defineOptions({ name: 'InfraDemoStudent' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+const currentRow = ref({}) // 选中行
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
+    list.value = [
+      {
+        id: 1
+      },
+      {
+        id: 10
+      }
+    ]
+    total.value = 10
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+// const demoStudentContactFormRef = ref()
+const openForm = (type: string, id?: number) => {
+  // console.log(demoStudentContactFormRef, 'xx demoStudentContactFormRef xx')
+  // demoStudentContactFormRef.value.validate()
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DemoStudentApi.deleteDemoStudent(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await DemoStudentApi.exportDemoStudent(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 选中行操作 */
+const handleCurrentChange = (row) => {
+  console.log(currentRow.value, '==== currentRow.value ====')
+  console.log(row, '==== row ====')
+  currentRow.value = row
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

From 55f6e4ca25eb246d6c49327e0b23b6839d187c19 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 10 Nov 2023 23:01:36 +0800
Subject: [PATCH 05/15] =?UTF-8?q?=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=9A?=
 =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=88=97=E8=A1=A8=E7=BC=96=E8=BE=91=E7=9A=84?=
 =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BB=84=E4=BB=B6=E6=83=85=E5=86=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/UploadFile/src/UploadFile.vue  |  2 +
 .../infra/demo02/DemoStudentContactForm.vue   | 68 +++++++++++++++++--
 2 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue
index 6895440b..bba466a7 100644
--- a/src/components/UploadFile/src/UploadFile.vue
+++ b/src/components/UploadFile/src/UploadFile.vue
@@ -144,6 +144,8 @@ watch(
     } else if (isArray(props.modelValue)) {
       // 情况2:字符串
       files.concat(props.modelValue)
+    } else if (props.modelValue === undefined) {
+      // 情况3:undefined 不处理
     } else {
       throw new Error('不支持的 modelValue 类型')
     }
diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
index 50c44263..694aadac 100644
--- a/src/views/infra/demo02/DemoStudentContactForm.vue
+++ b/src/views/infra/demo02/DemoStudentContactForm.vue
@@ -9,20 +9,73 @@
   >
     <el-table :data="formData" class="-mt-10px">
       <el-table-column label="序号" type="index" width="100" />
-      <el-table-column label="名字" prop="name" width="300">
+      <el-table-column label="名字" prop="name" width="50">
         <template #default="row">
           <el-form-item class="mb-0px!">
             <el-input v-model="row.name" placeholder="请输入名字" />
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="手机号码">
+      <el-table-column label="手机号码" width="150">
         <template #default="{ row, $index }">
           <el-form-item :prop="`${$index}.mobile`" :rules="formRules.mobile" class="mb-0px!">
             <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
           </el-form-item>
         </template>
       </el-table-column>
+      <el-table-column label="类型" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.javaType`" :rules="formRules.javaType" class="mb-0px!">
+            <el-select v-model="row.javaType">
+              <el-option label="Long" value="Long" />
+              <el-option label="String" value="String" />
+              <el-option label="Integer" value="Integer" />
+              <el-option label="Double" value="Double" />
+              <el-option label="BigDecimal" value="BigDecimal" />
+              <el-option label="LocalDateTime" value="LocalDateTime" />
+              <el-option label="Boolean" value="Boolean" />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="多选" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
+            <el-checkbox-group v-model="row.sex">
+              <el-checkbox key="Long" label="Long">Long</el-checkbox>
+              <el-checkbox key="String" label="String">String</el-checkbox>
+            </el-checkbox-group>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="图片上传" width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.pic`" :rules="formRules.pic" class="mb-0px!">
+            <UploadImg v-model="row.pic" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="文件上传" width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.file`" :rules="formRules.file" class="mb-0px!">
+            <UploadFile v-model="row.file" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="大输入框" width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.content`" :rules="formRules.content" class="mb-0px!">
+            <el-input v-model="row.content" type="textarea" placeholder="请输入 content" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="HTML" width="1024">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.html`" :rules="formRules.html" class="mb-0px!">
+            <Editor v-model="row.html" height="150px" />
+          </el-form-item>
+        </template>
+      </el-table-column>
       <el-table-column align="center" fixed="right" label="操作" width="60">
         <template #default="{ $index }">
           <el-button @click="handleDelete($index)" link>—</el-button>
@@ -41,7 +94,10 @@ const props = defineProps<{
 const formLoading = ref(false) // 表单的加载中
 const formData = ref([])
 const formRules = reactive({
-  mobile: [required]
+  mobile: [required],
+  javaType: [required],
+  sex: [required],
+  pic: [required]
 })
 const formRef = ref() // 表单 Ref
 
@@ -53,7 +109,11 @@ watch(
       formData.value = [
         {
           name: '芋艿',
-          mobile: '15601691300'
+          mobile: '15601691300',
+          javaType: undefined,
+          sex: [],
+          pic: undefined,
+          file: undefined
         }
       ]
     } else {

From 72c5eaae31fd37dcfcd016224351237a9e2004d7 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 12 Nov 2023 19:50:38 +0800
Subject: [PATCH 06/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=20one=20=E6=83=85=E5=86=B5=E4=B8=8B?=
 =?UTF-8?q?=E7=9A=84=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo01/index.ts                 |  43 +++
 src/api/infra/demo02/index.ts                 |  14 +
 src/components/UploadFile/src/UploadFile.vue  |   2 +-
 src/views/infra/demo01/Demo01StudentForm.vue  | 150 +++++++++++
 src/views/infra/demo01/index.vue              | 251 ++++++++++++++++++
 .../infra/demo02/DemoStudentAddressForm.vue   | 100 +++++--
 .../infra/demo02/DemoStudentContactForm.vue   | 158 ++++++-----
 src/views/infra/demo02/DemoStudentForm.vue    |  35 +--
 .../demo02/bak/DemoStudentAddressForm.vue     |  58 ++++
 9 files changed, 702 insertions(+), 109 deletions(-)
 create mode 100644 src/api/infra/demo01/index.ts
 create mode 100644 src/views/infra/demo01/Demo01StudentForm.vue
 create mode 100644 src/views/infra/demo01/index.vue
 create mode 100644 src/views/infra/demo02/bak/DemoStudentAddressForm.vue

diff --git a/src/api/infra/demo01/index.ts b/src/api/infra/demo01/index.ts
new file mode 100644
index 00000000..0154bc29
--- /dev/null
+++ b/src/api/infra/demo01/index.ts
@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+export interface Demo01StudentVO {
+  id: number
+  name: string
+  description: string
+  birthday: Date
+  sex: number
+  enabled: boolean
+  avatar: string
+  video: string
+  memo: string
+}
+
+// 查询学生列表
+export const getDemo01StudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo01-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemo01Student = async (id: number) => {
+  return await request.get({ url: `/infra/demo01-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemo01Student = async (data: Demo01StudentVO) => {
+  return await request.post({ url: `/infra/demo01-student/create`, data })
+}
+
+// 修改学生
+export const updateDemo01Student = async (data: Demo01StudentVO) => {
+  return await request.put({ url: `/infra/demo01-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemo01Student = async (id: number) => {
+  return await request.delete({ url: `/infra/demo01-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemo01Student = async (params) => {
+  return await request.download({ url: `/infra/demo01-student/export-excel`, params })
+}
\ No newline at end of file
diff --git a/src/api/infra/demo02/index.ts b/src/api/infra/demo02/index.ts
index b575dba4..500adde5 100644
--- a/src/api/infra/demo02/index.ts
+++ b/src/api/infra/demo02/index.ts
@@ -33,3 +33,17 @@ export const deleteDemoStudent = async (id: number) => {
 export const exportDemoStudent = async (params) => {
   return await request.download({ url: `/infra/demo-student/export-excel`, params })
 }
+
+// 获得学生联系人列表
+export const getDemoStudentContactListByStudentId = async (studentId) => {
+  return await request.get({
+    url: `/infra/demo-student/demo-student/list-by-student-id?studentId=` + studentId
+  })
+}
+
+// 获得学生地址
+export const getDemoStudentAddressByStudentId = async (studentId) => {
+  return await request.get({
+    url: `/infra/demo-student/demo-student/get-by-student-id?studentId=` + studentId
+  })
+}
diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue
index bba466a7..c1f3e4e2 100644
--- a/src/components/UploadFile/src/UploadFile.vue
+++ b/src/components/UploadFile/src/UploadFile.vue
@@ -144,7 +144,7 @@ watch(
     } else if (isArray(props.modelValue)) {
       // 情况2:字符串
       files.concat(props.modelValue)
-    } else if (props.modelValue === undefined) {
+    } else if (props.modelValue == null) {
       // 情况3:undefined 不处理
     } else {
       throw new Error('不支持的 modelValue 类型')
diff --git a/src/views/infra/demo01/Demo01StudentForm.vue b/src/views/infra/demo01/Demo01StudentForm.vue
new file mode 100644
index 00000000..dfc9823d
--- /dev/null
+++ b/src/views/infra/demo01/Demo01StudentForm.vue
@@ -0,0 +1,150 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <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
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo01StudentApi from '@/api/infra/demo01'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo01StudentApi.getDemo01Student(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as Demo01StudentApi.Demo01StudentVO
+    if (formType.value === 'create') {
+      await Demo01StudentApi.createDemo01Student(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo01StudentApi.updateDemo01Student(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo01/index.vue b/src/views/infra/demo01/index.vue
new file mode 100644
index 00000000..fb35b561
--- /dev/null
+++ b/src/views/infra/demo01/index.vue
@@ -0,0 +1,251 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="queryParams.birthday"
+          value-format="YYYY-MM-DD"
+          type="date"
+          placeholder="选择出生日期"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select
+          v-model="queryParams.enabled"
+          placeholder="请选择是否有效"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo01-student:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo01-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo01-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo01-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <Demo01StudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as Demo01StudentApi from '@/api/infra/demo01'
+import Demo01StudentForm from './Demo01StudentForm.vue'
+
+defineOptions({ name: 'InfraDemo01Student' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  birthday: null,
+  birthday: [],
+  sex: null,
+  enabled: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo01StudentApi.getDemo01StudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo01StudentApi.deleteDemo01Student(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await Demo01StudentApi.exportDemo01Student(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo02/DemoStudentAddressForm.vue b/src/views/infra/demo02/DemoStudentAddressForm.vue
index af4cd8f1..6d031406 100644
--- a/src/views/infra/demo02/DemoStudentAddressForm.vue
+++ b/src/views/infra/demo02/DemoStudentAddressForm.vue
@@ -6,39 +6,107 @@
     label-width="100px"
     v-loading="formLoading"
   >
-    <el-form-item label="子字段 1" prop="field1">
-      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+    <el-form-item label="名字" prop="name">
+      <el-input v-model="formData.name" placeholder="请输入名字" />
     </el-form-item>
-    <el-form-item label="子字段 2" prop="field2">
-      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+    <el-form-item label="个人简介">
+      <Editor v-model="formData.description" height="150px" />
     </el-form-item>
-    <el-form-item label="子字段 3" prop="field3">
-      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+    <el-form-item label="性别 1" prop="sex1">
+      <el-select v-model="formData.sex1" placeholder="请选择性别 1">
+        <el-option
+          v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SEX1)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="性别 2" prop="sex2">
+      <el-checkbox-group v-model="formData.sex2">
+        <el-checkbox
+          v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SEX2)"
+          :key="dict.value"
+          :label="dict.value"
+        >
+          {{ dict.label }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </el-form-item>
+    <el-form-item label="性别 3" prop="sex3">
+      <el-radio-group v-model="formData.sex3">
+        <el-radio
+          v-for="dict in getBoolDictOptions(DICT_TYPE.SYSTEM_SEX3)"
+          :key="dict.value"
+          :label="dict.value"
+        >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="出生日期" prop="birthday">
+      <el-date-picker
+        v-model="formData.birthday"
+        type="date"
+        value-format="x"
+        placeholder="选择出生日期"
+      />
+    </el-form-item>
+    <el-form-item label="备注" prop="memo">
+      <el-input v-model="formData.memo" type="textarea" placeholder="请输入备注" />
     </el-form-item>
   </el-form>
 </template>
 <script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as DemoStudentApi from '@/api/infra/demo02'
+
 const props = defineProps<{
-  studentId: undefined // 学生编号
+  studentId: undefined // 学生编号(主表的关联字段)
 }>()
 const formLoading = ref(false) // 表单的加载中
-const formData = ref({})
+const formData = ref([])
 const formRules = reactive({
-  field1: [required]
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  video: [{ required: true, message: '视频不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '个人简介不能为空', trigger: 'blur' }],
+  sex1: [{ required: true, message: '性别 1不能为空', trigger: 'change' }],
+  sex2: [{ required: true, message: '性别 2不能为空', trigger: 'blur' }],
+  sex3: [{ required: true, message: '性别 3不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
 /** 监听主表的关联字段的变化,加载对应的子表数据 */
 watch(
   () => props.studentId,
-  (val) => {
-    if (val) {
+  async (val) => {
+    // 情况一:val 为空,说明是新增,则置空
+    if (!val) {
       formData.value = {
-        field2: '番茄',
-        field3: '西瓜'
+        id: undefined,
+        studentId: undefined,
+        name: undefined,
+        avatar: undefined,
+        video: undefined,
+        description: undefined,
+        sex1: undefined,
+        sex2: [],
+        sex3: undefined,
+        birthday: undefined,
+        memo: undefined
       }
-    } else {
-      formData.value = {}
+      return
+    }
+    // 情况二:val 非空,说明是修改,则加载数据
+    try {
+      formLoading.value = true
+      formData.value = await DemoStudentApi.getDemoStudentAddressByStudentId(val)
+    } finally {
+      formLoading.value = false
     }
   },
   { immediate: true }
@@ -49,7 +117,7 @@ const validate = () => {
   return formRef.value.validate()
 }
 
-/** 表单值 **/
+/** 表单值 */
 const getData = () => {
   return formData.value
 }
diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
index 694aadac..5f173f14 100644
--- a/src/views/infra/demo02/DemoStudentContactForm.vue
+++ b/src/views/infra/demo02/DemoStudentContactForm.vue
@@ -3,84 +3,93 @@
     ref="formRef"
     :model="formData"
     :rules="formRules"
-    label-width="0px"
     v-loading="formLoading"
+    label-width="0px"
     :inline-message="true"
   >
     <el-table :data="formData" class="-mt-10px">
       <el-table-column label="序号" type="index" width="100" />
-      <el-table-column label="名字" prop="name" width="50">
-        <template #default="row">
-          <el-form-item class="mb-0px!">
+      <el-table-column label="名字" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
             <el-input v-model="row.name" placeholder="请输入名字" />
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="手机号码" width="150">
+      <el-table-column label="个人简介" width="400">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.mobile`" :rules="formRules.mobile" class="mb-0px!">
-            <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
+          <el-form-item
+            :prop="`${$index}.description`"
+            :rules="formRules.description"
+            class="mb-0px!"
+          >
+            <Editor v-model="row.description" height="150px" />
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="类型" width="150">
+      <el-table-column label="性别 1" width="150">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.javaType`" :rules="formRules.javaType" class="mb-0px!">
-            <el-select v-model="row.javaType">
-              <el-option label="Long" value="Long" />
-              <el-option label="String" value="String" />
-              <el-option label="Integer" value="Integer" />
-              <el-option label="Double" value="Double" />
-              <el-option label="BigDecimal" value="BigDecimal" />
-              <el-option label="LocalDateTime" value="LocalDateTime" />
-              <el-option label="Boolean" value="Boolean" />
+          <el-form-item :prop="`${$index}.sex1`" :rules="formRules.sex1" class="mb-0px!">
+            <el-select v-model="row.sex1" placeholder="请选择性别 1">
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SEX1)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
             </el-select>
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="多选" width="150">
+      <el-table-column label="性别 2" width="150">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
-            <el-checkbox-group v-model="row.sex">
-              <el-checkbox key="Long" label="Long">Long</el-checkbox>
-              <el-checkbox key="String" label="String">String</el-checkbox>
+          <el-form-item :prop="`${$index}.sex2`" :rules="formRules.sex2" class="mb-0px!">
+            <el-checkbox-group v-model="row.sex2">
+              <el-checkbox
+                v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SEX2)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-checkbox>
             </el-checkbox-group>
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="图片上传" width="200">
+      <el-table-column label="性别 3" width="150">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.pic`" :rules="formRules.pic" class="mb-0px!">
-            <UploadImg v-model="row.pic" />
+          <el-form-item :prop="`${$index}.sex3`" :rules="formRules.sex3" class="mb-0px!">
+            <el-radio-group v-model="row.sex3">
+              <el-radio
+                v-for="dict in getBoolDictOptions(DICT_TYPE.SYSTEM_SEX3)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="文件上传" width="200">
+      <el-table-column label="出生日期" width="150">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.file`" :rules="formRules.file" class="mb-0px!">
-            <UploadFile v-model="row.file" />
+          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+            <el-date-picker
+              v-model="row.birthday"
+              type="date"
+              value-format="x"
+              placeholder="选择出生日期"
+            />
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="大输入框" width="200">
+      <el-table-column label="备注" width="200">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.content`" :rules="formRules.content" class="mb-0px!">
-            <el-input v-model="row.content" type="textarea" placeholder="请输入 content" />
+          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+            <el-input v-model="row.memo" type="textarea" placeholder="请输入备注" />
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="HTML" width="1024">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.html`" :rules="formRules.html" class="mb-0px!">
-            <Editor v-model="row.html" height="150px" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" fixed="right" label="操作" width="60">
-        <template #default="{ $index }">
-          <el-button @click="handleDelete($index)" link>—</el-button>
-        </template>
-      </el-table-column>
     </el-table>
   </el-form>
   <el-row justify="center" class="mt-3">
@@ -88,36 +97,43 @@
   </el-row>
 </template>
 <script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as DemoStudentApi from '@/api/infra/demo02'
+
 const props = defineProps<{
-  studentId: undefined // 学生编号
+  studentId: undefined // 学生编号(主表的关联字段)
 }>()
 const formLoading = ref(false) // 表单的加载中
 const formData = ref([])
 const formRules = reactive({
-  mobile: [required],
-  javaType: [required],
-  sex: [required],
-  pic: [required]
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  video: [{ required: true, message: '视频不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '个人简介不能为空', trigger: 'blur' }],
+  sex1: [{ required: true, message: '性别 1不能为空', trigger: 'change' }],
+  sex2: [{ required: true, message: '性别 2不能为空', trigger: 'blur' }],
+  sex3: [{ required: true, message: '性别 3不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
 /** 监听主表的关联字段的变化,加载对应的子表数据 */
 watch(
   () => props.studentId,
-  (val) => {
-    if (val) {
-      formData.value = [
-        {
-          name: '芋艿',
-          mobile: '15601691300',
-          javaType: undefined,
-          sex: [],
-          pic: undefined,
-          file: undefined
-        }
-      ]
-    } else {
+  async (val) => {
+    // 情况一:val 为空,说明是新增,则置空
+    if (!val) {
       formData.value = []
+      return
+    }
+    // 情况二:val 非空,说明是修改,则加载数据
+    try {
+      formLoading.value = true
+      formData.value = await DemoStudentApi.getDemoStudentContactListByStudentId(val)
+    } finally {
+      formLoading.value = false
     }
   },
   { immediate: true }
@@ -125,9 +141,21 @@ watch(
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-  formData.value.push({
-    name: '土豆'
-  })
+  const row = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    avatar: undefined,
+    video: undefined,
+    description: undefined,
+    sex1: undefined,
+    sex2: [],
+    sex3: undefined,
+    birthday: undefined,
+    memo: undefined
+  }
+  row.studentId = props.studentId
+  formData.value.push(row)
 }
 
 /** 删除按钮操作 */
@@ -140,7 +168,7 @@ const validate = () => {
   return formRef.value.validate()
 }
 
-/** 表单值 **/
+/** 表单值 */
 const getData = () => {
   return formData.value
 }
diff --git a/src/views/infra/demo02/DemoStudentForm.vue b/src/views/infra/demo02/DemoStudentForm.vue
index fe0788c4..e9454c0f 100644
--- a/src/views/infra/demo02/DemoStudentForm.vue
+++ b/src/views/infra/demo02/DemoStudentForm.vue
@@ -6,23 +6,13 @@
       :rules="formRules"
       label-width="100px"
       v-loading="formLoading"
-    >
-      <el-form-item label="字段 1" prop="field1">
-        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-      </el-form-item>
-      <el-form-item label="字段 2" prop="field2">
-        <el-input v-model="formData.field2" placeholder="请输入字段 2" />
-      </el-form-item>
-      <el-form-item label="字段 3" prop="field3">
-        <el-input v-model="formData.field3" placeholder="请输入字段 3" />
-      </el-form-item>
-    </el-form>
+    />
     <!-- 子表的表单 -->
     <el-tabs v-model="subTabsName">
-      <el-tab-pane label="联系人信息" name="DemoStudentContact">
+      <el-tab-pane label="学生联系人" name="demoStudentContact">
         <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
       </el-tab-pane>
-      <el-tab-pane label="地址信息" name="DemoStudentAddress">
+      <el-tab-pane label="学生地址" name="demoStudentAddress">
         <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
       </el-tab-pane>
     </el-tabs>
@@ -47,15 +37,13 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined
 })
-const formRules = reactive({
-  field2: [required]
-})
+const formRules = reactive({})
 const formRef = ref() // 表单 Ref
 
 /** 子表的表单 */
+const subTabsName = ref('demoStudentContact')
 const demoStudentContactFormRef = ref()
 const demoStudentAddressFormRef = ref()
-const subTabsName = ref('DemoStudentContact')
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -65,16 +53,9 @@ const open = async (type: string, id?: number) => {
   resetForm()
   // 修改时,设置数据
   if (id) {
-    // debugger
     formLoading.value = true
     try {
-      // formData.value = await DemoStudentApi.getDemoStudent(id)
-      formData.value = {
-        id: id,
-        field1: '1',
-        field2: '22',
-        field3: '333'
-      }
+      formData.value = await DemoStudentApi.getDemoStudent(id)
     } finally {
       formLoading.value = false
     }
@@ -91,13 +72,13 @@ const submitForm = async () => {
   try {
     await demoStudentContactFormRef.value.validate()
   } catch (e) {
-    subTabsName.value = 'DemoStudentContact'
+    subTabsName.value = 'demoStudentContact'
     return
   }
   try {
     await demoStudentAddressFormRef.value.validate()
   } catch (e) {
-    subTabsName.value = 'DemoStudentAddress'
+    subTabsName.value = 'demoStudentAddress'
     return
   }
   // 提交请求
diff --git a/src/views/infra/demo02/bak/DemoStudentAddressForm.vue b/src/views/infra/demo02/bak/DemoStudentAddressForm.vue
new file mode 100644
index 00000000..af4cd8f1
--- /dev/null
+++ b/src/views/infra/demo02/bak/DemoStudentAddressForm.vue
@@ -0,0 +1,58 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+    <el-form-item label="子字段 1" prop="field1">
+      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
+    </el-form-item>
+    <el-form-item label="子字段 2" prop="field2">
+      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
+    </el-form-item>
+    <el-form-item label="子字段 3" prop="field3">
+      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+const props = defineProps<{
+  studentId: undefined // 学生编号
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref({})
+const formRules = reactive({
+  field1: [required]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    if (val) {
+      formData.value = {
+        field2: '番茄',
+        field3: '西瓜'
+      }
+    } else {
+      formData.value = {}
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 **/
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

From f0f78860255d1b3190556cd4dbd500c69d8a4b1f Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Sun, 12 Nov 2023 22:33:38 +0800
Subject: [PATCH 07/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=88?=
 =?UTF-8?q?=E6=99=AE=E9=80=9A=EF=BC=89=E7=9A=84=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .eslintrc.js                                  |   5 +-
 src/api/infra/demo11/index.ts                 |  57 ++++
 src/views/infra/demo11/Demo11StudentForm.vue  | 183 +++++++++++++
 .../components/Demo11StudentContactForm.vue   | 169 ++++++++++++
 .../components/Demo11StudentTeacherForm.vue   | 122 +++++++++
 src/views/infra/demo11/index.vue              | 252 ++++++++++++++++++
 6 files changed, 786 insertions(+), 2 deletions(-)
 create mode 100644 src/api/infra/demo11/index.ts
 create mode 100644 src/views/infra/demo11/Demo11StudentForm.vue
 create mode 100644 src/views/infra/demo11/components/Demo11StudentContactForm.vue
 create mode 100644 src/views/infra/demo11/components/Demo11StudentTeacherForm.vue
 create mode 100644 src/views/infra/demo11/index.vue

diff --git a/.eslintrc.js b/.eslintrc.js
index f2977df6..70c91784 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -21,7 +21,7 @@ module.exports = defineConfig({
     'plugin:vue/vue3-recommended',
     'plugin:@typescript-eslint/recommended',
     'prettier',
-    'plugin:prettier/recommended', 
+    'plugin:prettier/recommended',
     '@unocss'
   ],
   rules: {
@@ -67,6 +67,7 @@ module.exports = defineConfig({
       }
     ],
     'vue/multi-word-component-names': 'off',
-    'vue/no-v-html': 'off'
+    'vue/no-v-html': 'off',
+    'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
   }
 })
diff --git a/src/api/infra/demo11/index.ts b/src/api/infra/demo11/index.ts
new file mode 100644
index 00000000..4c4c7b4e
--- /dev/null
+++ b/src/api/infra/demo11/index.ts
@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+export interface Demo11StudentVO {
+  id: number
+  name: string
+  description: string
+  birthday: Date
+  sex: number
+  enabled: boolean
+  avatar: string
+  video: string
+  memo: string
+}
+
+// 查询学生列表
+export const getDemo11StudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo11-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemo11Student = async (id: number) => {
+  return await request.get({ url: `/infra/demo11-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemo11Student = async (data: Demo11StudentVO) => {
+  return await request.post({ url: `/infra/demo11-student/create`, data })
+}
+
+// 修改学生
+export const updateDemo11Student = async (data: Demo11StudentVO) => {
+  return await request.put({ url: `/infra/demo11-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemo11Student = async (id: number) => {
+  return await request.delete({ url: `/infra/demo11-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemo11Student = async (params) => {
+  return await request.download({ url: `/infra/demo11-student/export-excel`, params })
+}
+
+// ==================== 子表(学生联系人) ====================
+
+// 获得学生联系人列表
+export const getDemo11StudentContactListByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo11-student/demo11-student/list-by-student-id?studentId=` + studentId })
+}
+
+// ==================== 子表(学生班主任) ====================
+
+// 获得学生班主任
+export const getDemo11StudentTeacherByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo11-student/demo11-student/get-by-student-id?studentId=` + studentId })
+}
\ No newline at end of file
diff --git a/src/views/infra/demo11/Demo11StudentForm.vue b/src/views/infra/demo11/Demo11StudentForm.vue
new file mode 100644
index 00000000..39c21a2b
--- /dev/null
+++ b/src/views/infra/demo11/Demo11StudentForm.vue
@@ -0,0 +1,183 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <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
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="学生联系人" name="demo11StudentContact">
+        <Demo11StudentContactForm ref="demo11StudentContactFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班主任" name="demo11StudentTeacher">
+        <Demo11StudentTeacherForm ref="demo11StudentTeacherFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo11StudentApi from '@/api/infra/demo11'
+import Demo11StudentContactForm from './components/Demo11StudentContactForm.vue'
+import Demo11StudentTeacherForm from './components/Demo11StudentTeacherForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 子表的表单 */
+const subTabsName = ref('demo11StudentContact')
+const demo11StudentContactFormRef = ref()
+const demo11StudentTeacherFormRef = ref()
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo11StudentApi.getDemo11Student(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demo11StudentContactFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo11StudentContact'
+    return
+  }
+  try {
+    await demo11StudentTeacherFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo11StudentTeacher'
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as Demo11StudentApi.Demo11StudentVO
+    // 拼接子表的数据
+    data.demo11StudentContacts = demo11StudentContactFormRef.value.getData()
+    data.demo11StudentTeacher = demo11StudentTeacherFormRef.value.getData()
+    if (formType.value === 'create') {
+      await Demo11StudentApi.createDemo11Student(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo11StudentApi.updateDemo11Student(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo11/components/Demo11StudentContactForm.vue b/src/views/infra/demo11/components/Demo11StudentContactForm.vue
new file mode 100644
index 00000000..1ab017e8
--- /dev/null
+++ b/src/views/infra/demo11/components/Demo11StudentContactForm.vue
@@ -0,0 +1,169 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+      <el-table-column label="名字" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+            <el-input v-model="row.name" placeholder="请输入名字" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
+            <el-input v-model="row.description" type="textarea" placeholder="请输入简介" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+            <el-date-picker
+              v-model="row.birthday"
+              type="date"
+              value-format="x"
+              placeholder="选择出生日期"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
+            <el-select v-model="row.sex" placeholder="请选择性别">
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.enabled`" :rules="formRules.enabled" class="mb-0px!">
+            <el-radio-group v-model="row.enabled">
+                <el-radio
+                  v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                  :key="dict.value"
+                  :label="dict.value"
+                >
+                  {{ dict.label }}
+                </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+            <UploadImg v-model="row.avatar" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+            <UploadFile v-model="row.video" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" width="400">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+            <Editor v-model="row.memo" height="150px" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo11StudentApi from '@/api/infra/demo11'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 情况一:val 为空,说明是新增,则置空
+    if (!val) {
+      formData.value = []
+      return;
+    }
+    // 情况二:val 非空,说明是修改,则加载数据
+    try {
+      formLoading.value = true
+      formData.value = await Demo11StudentApi.getDemo11StudentContactListByStudentId(val)
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  row.studentId = props.studentId
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo11/components/Demo11StudentTeacherForm.vue b/src/views/infra/demo11/components/Demo11StudentTeacherForm.vue
new file mode 100644
index 00000000..a8697be1
--- /dev/null
+++ b/src/views/infra/demo11/components/Demo11StudentTeacherForm.vue
@@ -0,0 +1,122 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+    <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
+        v-model="formData.birthday"
+        type="date"
+        value-format="x"
+        placeholder="选择出生日期"
+      />
+    </el-form-item>
+    <el-form-item label="性别" prop="sex">
+      <el-select v-model="formData.sex" placeholder="请选择性别">
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="是否有效" prop="enabled">
+      <el-radio-group v-model="formData.enabled">
+        <el-radio
+          v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+          :key="dict.value"
+          :label="dict.value"
+          >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="头像" prop="avatar">
+      <UploadImg v-model="formData.avatar" />
+    </el-form-item>
+    <el-form-item label="附件" prop="video">
+      <UploadFile v-model="formData.video" />
+    </el-form-item>
+    <el-form-item label="备注" prop="memo">
+      <Editor v-model="formData.memo" height="150px" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo11StudentApi from '@/api/infra/demo11'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = {
+      id: undefined,
+      studentId: undefined,
+      name: undefined,
+      description: undefined,
+      birthday: undefined,
+      sex: undefined,
+      enabled: undefined,
+      avatar: undefined,
+      video: undefined,
+      memo: undefined,
+    }
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      const data = await Demo11StudentApi.getDemo11StudentTeacherByStudentId(val)
+      if (!data) {
+        return
+      }
+      formData.value = data
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo11/index.vue b/src/views/infra/demo11/index.vue
new file mode 100644
index 00000000..36529eb9
--- /dev/null
+++ b/src/views/infra/demo11/index.vue
@@ -0,0 +1,252 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="queryParams.birthday"
+          value-format="YYYY-MM-DD"
+          type="date"
+          placeholder="选择出生日期"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select
+          v-model="queryParams.enabled"
+          placeholder="请选择是否有效"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo11-student:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo11-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo11-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo11-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <Demo11StudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as Demo11StudentApi from '@/api/infra/demo11'
+import Demo11StudentForm from './Demo11StudentForm.vue'
+
+defineOptions({ name: 'InfraDemo11Student' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  birthday: null,
+  birthday: [],
+  sex: null,
+  enabled: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo11StudentApi.getDemo11StudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo11StudentApi.deleteDemo11Student(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await Demo11StudentApi.exportDemo11Student(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file

From 999d8de9e972d62ae4bf2a23bcb05d7559c8c101 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Mon, 13 Nov 2023 13:57:15 +0800
Subject: [PATCH 08/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=B8=BB=E5=AD=90=E8=A1=A8=EF=BC=88?=
 =?UTF-8?q?=E5=86=85=E5=B5=8C=EF=BC=89=E7=9A=84=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo12/index.ts                 |  57 ++++
 .../components/Demo11StudentContactForm.vue   |   5 +
 src/views/infra/demo12/Demo12StudentForm.vue  | 183 +++++++++++++
 .../components/Demo12StudentContactForm.vue   |   5 +
 .../components/Demo12StudentContactList.vue   |  63 +++++
 .../components/Demo12StudentTeacherForm.vue   |   5 +
 .../components/Demo12StudentTeacherList.vue   |  67 +++++
 src/views/infra/demo12/index.vue              | 252 ++++++++++++++++++
 8 files changed, 637 insertions(+)
 create mode 100644 src/api/infra/demo12/index.ts
 create mode 100644 src/views/infra/demo12/Demo12StudentForm.vue
 create mode 100644 src/views/infra/demo12/components/Demo12StudentContactForm.vue
 create mode 100644 src/views/infra/demo12/components/Demo12StudentContactList.vue
 create mode 100644 src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
 create mode 100644 src/views/infra/demo12/components/Demo12StudentTeacherList.vue
 create mode 100644 src/views/infra/demo12/index.vue

diff --git a/src/api/infra/demo12/index.ts b/src/api/infra/demo12/index.ts
new file mode 100644
index 00000000..33c3fb4f
--- /dev/null
+++ b/src/api/infra/demo12/index.ts
@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+export interface Demo12StudentVO {
+  id: number
+  name: string
+  description: string
+  birthday: Date
+  sex: number
+  enabled: boolean
+  avatar: string
+  video: string
+  memo: string
+}
+
+// 查询学生列表
+export const getDemo12StudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo12-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemo12Student = async (id: number) => {
+  return await request.get({ url: `/infra/demo12-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemo12Student = async (data: Demo12StudentVO) => {
+  return await request.post({ url: `/infra/demo12-student/create`, data })
+}
+
+// 修改学生
+export const updateDemo12Student = async (data: Demo12StudentVO) => {
+  return await request.put({ url: `/infra/demo12-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemo12Student = async (id: number) => {
+  return await request.delete({ url: `/infra/demo12-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemo12Student = async (params) => {
+  return await request.download({ url: `/infra/demo12-student/export-excel`, params })
+}
+
+// ==================== 子表(学生联系人) ====================
+
+// 获得学生联系人列表
+export const getDemo12StudentContactListByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo12-student/demo12-student/list-by-student-id?studentId=` + studentId })
+}
+
+// ==================== 子表(学生班主任) ====================
+
+// 获得学生班主任
+export const getDemo12StudentTeacherByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo12-student/demo12-student/get-by-student-id?studentId=` + studentId })
+}
\ No newline at end of file
diff --git a/src/views/infra/demo11/components/Demo11StudentContactForm.vue b/src/views/infra/demo11/components/Demo11StudentContactForm.vue
index 1ab017e8..ba900017 100644
--- a/src/views/infra/demo11/components/Demo11StudentContactForm.vue
+++ b/src/views/infra/demo11/components/Demo11StudentContactForm.vue
@@ -85,6 +85,11 @@
           </el-form-item>
         </template>
       </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
     </el-table>
   </el-form>
   <el-row justify="center" class="mt-3">
diff --git a/src/views/infra/demo12/Demo12StudentForm.vue b/src/views/infra/demo12/Demo12StudentForm.vue
new file mode 100644
index 00000000..d3cb482d
--- /dev/null
+++ b/src/views/infra/demo12/Demo12StudentForm.vue
@@ -0,0 +1,183 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <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
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="学生联系人" name="demo12StudentContact">
+        <Demo12StudentContactForm ref="demo12StudentContactFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班主任" name="demo12StudentTeacher">
+        <Demo12StudentTeacherForm ref="demo12StudentTeacherFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo12StudentApi from '@/api/infra/demo12'
+import Demo12StudentContactForm from './components/Demo12StudentContactForm.vue'
+import Demo12StudentTeacherForm from './components/Demo12StudentTeacherForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 子表的表单 */
+const subTabsName = ref('demo12StudentContact')
+const demo12StudentContactFormRef = ref()
+const demo12StudentTeacherFormRef = ref()
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo12StudentApi.getDemo12Student(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demo12StudentContactFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo12StudentContact'
+    return
+  }
+  try {
+    await demo12StudentTeacherFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo12StudentTeacher'
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as Demo12StudentApi.Demo12StudentVO
+    // 拼接子表的数据
+    data.demo12StudentContacts = demo12StudentContactFormRef.value.getData()
+    data.demo12StudentTeacher = demo12StudentTeacherFormRef.value.getData()
+    if (formType.value === 'create') {
+      await Demo12StudentApi.createDemo12Student(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo12StudentApi.updateDemo12Student(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentContactForm.vue b/src/views/infra/demo12/components/Demo12StudentContactForm.vue
new file mode 100644
index 00000000..d71f6146
--- /dev/null
+++ b/src/views/infra/demo12/components/Demo12StudentContactForm.vue
@@ -0,0 +1,5 @@
+<template>
+  <div>123</div>
+</template>
+<script setup lang="ts">
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentContactList.vue b/src/views/infra/demo12/components/Demo12StudentContactList.vue
new file mode 100644
index 00000000..65ed9df1
--- /dev/null
+++ b/src/views/infra/demo12/components/Demo12StudentContactList.vue
@@ -0,0 +1,63 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="学生编号" align="center" prop="studentId" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import * as Demo12StudentApi from '@/api/infra/demo12'
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await Demo12StudentApi.getDemo12StudentContactListByStudentId(studentId.props)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue b/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
new file mode 100644
index 00000000..d71f6146
--- /dev/null
+++ b/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
@@ -0,0 +1,5 @@
+<template>
+  <div>123</div>
+</template>
+<script setup lang="ts">
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentTeacherList.vue b/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
new file mode 100644
index 00000000..55f6845b
--- /dev/null
+++ b/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
@@ -0,0 +1,67 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="学生编号" align="center" prop="studentId" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import * as Demo12StudentApi from '@/api/infra/demo12'
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo12StudentApi.getDemo12StudentTeacherByStudentId(studentId.props)
+    if (!data) {
+      return
+    }
+    list.value.push(data)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/index.vue b/src/views/infra/demo12/index.vue
new file mode 100644
index 00000000..c5a82a4c
--- /dev/null
+++ b/src/views/infra/demo12/index.vue
@@ -0,0 +1,252 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="queryParams.birthday"
+          value-format="YYYY-MM-DD"
+          type="date"
+          placeholder="选择出生日期"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select
+          v-model="queryParams.enabled"
+          placeholder="请选择是否有效"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo12-student:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo12-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo12-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo12-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <Demo12StudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as Demo12StudentApi from '@/api/infra/demo12'
+import Demo12StudentForm from './Demo12StudentForm.vue'
+
+defineOptions({ name: 'InfraDemo12Student' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  birthday: null,
+  birthday: [],
+  sex: null,
+  enabled: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo12StudentApi.getDemo12StudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo12StudentApi.deleteDemo12Student(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await Demo12StudentApi.exportDemo12Student(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file

From 45656562a82353927da0aa614a1ce0689e7bd6b0 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Tue, 14 Nov 2023 09:50:54 +0800
Subject: [PATCH 09/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E6=94=AF=E6=8C=81=E8=AE=BE=E7=BD=AE=E4=B8=BB=E5=AD=90?=
 =?UTF-8?q?=E8=A1=A8=E7=9A=84=E4=BF=A1=E6=81=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/codegen/index.ts                |  10 +-
 src/api/infra/demo12/index.ts                 |  48 +++++-
 src/views/infra/codegen/EditTable.vue         |   2 +-
 .../codegen/components/GenerateInfoForm.vue   |  94 ++++++-----
 src/views/infra/demo12/Demo12StudentForm.vue  |   2 +-
 .../components/Demo12StudentContactForm.vue   | 152 +++++++++++++++++-
 .../components/Demo12StudentContactList.vue   |  95 ++++++++++-
 .../components/Demo12StudentTeacherForm.vue   | 152 +++++++++++++++++-
 .../components/Demo12StudentTeacherList.vue   |  99 ++++++++++--
 src/views/infra/demo12/index.vue              |  28 +++-
 10 files changed, 612 insertions(+), 70 deletions(-)

diff --git a/src/api/infra/codegen/index.ts b/src/api/infra/codegen/index.ts
index 64701efe..1b91e917 100644
--- a/src/api/infra/codegen/index.ts
+++ b/src/api/infra/codegen/index.ts
@@ -67,6 +67,11 @@ export type CodegenCreateListReqVO = {
   tableNames: string[]
 }
 
+// 查询列表代码生成表定义
+export const getCodegenTableList = (dataSourceConfigId: number) => {
+  return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId })
+}
+
 // 查询列表代码生成表定义
 export const getCodegenTablePage = (params: PageParam) => {
   return request.get({ url: '/infra/codegen/table/page', params })
@@ -92,11 +97,6 @@ export const syncCodegenFromDB = (id: number) => {
   return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id })
 }
 
-// 基于 SQL 建表语句,同步数据库的表和字段定义
-export const syncCodegenFromSQL = (id: number, sql: string) => {
-  return request.put({ url: '/infra/codegen/sync-from-sql?tableId=' + id + '&sql=' + sql })
-}
-
 // 预览生成代码
 export const previewCodegen = (id: number) => {
   return request.get({ url: '/infra/codegen/preview?tableId=' + id })
diff --git a/src/api/infra/demo12/index.ts b/src/api/infra/demo12/index.ts
index 33c3fb4f..3f2be47e 100644
--- a/src/api/infra/demo12/index.ts
+++ b/src/api/infra/demo12/index.ts
@@ -44,14 +44,52 @@ export const exportDemo12Student = async (params) => {
 
 // ==================== 子表(学生联系人) ====================
 
-// 获得学生联系人列表
-export const getDemo12StudentContactListByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo12-student/demo12-student/list-by-student-id?studentId=` + studentId })
+// 获得学生联系人分页
+export const getDemo12StudentContactPage = async (params) => {
+  return await request.get({ url: `/infra/demo12-student/demo12-student-contact/page`, params })
+}
+// 新增学生联系人
+export const createDemo12StudentContact = async (data) => {
+  return await request.post({ url: `/infra/demo12-student/demo12-student-contact/create`, data })
+}
+
+// 修改学生联系人
+export const updateDemo12StudentContact = async (data) => {
+  return await request.put({ url: `/infra/demo12-student/demo12-student-contact/update`, data })
+}
+
+// 删除学生联系人
+export const deleteDemo12StudentContact = async (id: number) => {
+  return await request.delete({ url: `/infra/demo12-student/demo12-student-contact/delete?id=` + id })
+}
+
+// 获得学生联系人
+export const getDemo12StudentContact = async (id: number) => {
+  return await request.get({ url: `/infra/demo12-student/demo12-student-contact/get?id=` + id })
 }
 
 // ==================== 子表(学生班主任) ====================
 
+// 获得学生班主任分页
+export const getDemo12StudentTeacherPage = async (params) => {
+  return await request.get({ url: `/infra/demo12-student/demo12-student-teacher/page`, params })
+}
+// 新增学生班主任
+export const createDemo12StudentTeacher = async (data) => {
+  return await request.post({ url: `/infra/demo12-student/demo12-student-teacher/create`, data })
+}
+
+// 修改学生班主任
+export const updateDemo12StudentTeacher = async (data) => {
+  return await request.put({ url: `/infra/demo12-student/demo12-student-teacher/update`, data })
+}
+
+// 删除学生班主任
+export const deleteDemo12StudentTeacher = async (id: number) => {
+  return await request.delete({ url: `/infra/demo12-student/demo12-student-teacher/delete?id=` + id })
+}
+
 // 获得学生班主任
-export const getDemo12StudentTeacherByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo12-student/demo12-student/get-by-student-id?studentId=` + studentId })
+export const getDemo12StudentTeacher = async (id: number) => {
+  return await request.get({ url: `/infra/demo12-student/demo12-student-teacher/get?id=` + id })
 }
\ No newline at end of file
diff --git a/src/views/infra/codegen/EditTable.vue b/src/views/infra/codegen/EditTable.vue
index 9c4e7657..c94e0da6 100644
--- a/src/views/infra/codegen/EditTable.vue
+++ b/src/views/infra/codegen/EditTable.vue
@@ -8,7 +8,7 @@
         <colum-info-form ref="columInfoRef" :columns="formData.columns" />
       </el-tab-pane>
       <el-tab-pane label="生成信息" name="generateInfo">
-        <generate-info-form ref="generateInfoRef" :table="formData.table" />
+        <generate-info-form ref="generateInfoRef" :table="formData.table" :columns="formData.columns" />
       </el-tab-pane>
     </el-tabs>
     <el-form>
diff --git a/src/views/infra/codegen/components/GenerateInfoForm.vue b/src/views/infra/codegen/components/GenerateInfoForm.vue
index 744edfe6..178b4e0f 100644
--- a/src/views/infra/codegen/components/GenerateInfoForm.vue
+++ b/src/views/infra/codegen/components/GenerateInfoForm.vue
@@ -3,7 +3,7 @@
     <el-row>
       <el-col :span="12">
         <el-form-item label="生成模板" prop="templateType">
-          <el-select v-model="formData.templateType" @change="tplSelectChange">
+          <el-select v-model="formData.templateType">
             <el-option
               v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
               :key="dict.value"
@@ -246,48 +246,68 @@
         </el-form-item>
       </el-col>
     </el-row>
-    <el-row v-show="formData.tplCategory === 'sub'">
-      <h4 class="form-header">关联信息</h4>
+
+    <!-- 主表信息 -->
+    <el-row v-if="formData.templateType === 15">
+      <el-col :span="24">
+        <h4 class="form-header">主表信息</h4>
+      </el-col>
       <el-col :span="12">
-        <el-form-item>
+        <el-form-item prop="masterTableId">
           <template #label>
             <span>
-              关联子表的表名
-              <el-tooltip content="关联子表的表名, 如:sys_user" placement="top">
+              关联的主表
+              <el-tooltip content="关联主表(父表)的表名, 如:system_user" placement="top">
                 <Icon icon="ep:question-filled" />
               </el-tooltip>
             </span>
           </template>
-          <el-select v-model="formData.subTableName" placeholder="请选择" @change="subSelectChange">
+          <el-select v-model="formData.masterTableId" placeholder="请选择">
             <el-option
               v-for="(table0, index) in tables"
               :key="index"
               :label="table0.tableName + ':' + table0.tableComment"
-              :value="table0.tableName"
+              :value="table0.id"
             />
           </el-select>
         </el-form-item>
       </el-col>
       <el-col :span="12">
-        <el-form-item>
+        <el-form-item prop="subJoinColumnId">
           <template #label>
             <span>
-              子表关联的外键名
-              <el-tooltip content="子表关联的外键名, 如:user_id" placement="top">
+              子表关联的字段
+              <el-tooltip content="子表关联的字段, 如:user_id" placement="top">
                 <Icon icon="ep:question-filled" />
               </el-tooltip>
             </span>
           </template>
-          <el-select v-model="formData.subTableFkName" placeholder="请选择">
+          <el-select v-model="formData.subJoinColumnId" placeholder="请选择">
             <el-option
-              v-for="(column, index) in subColumns"
+              v-for="(column, index) in props.columns"
               :key="index"
               :label="column.columnName + ':' + column.columnComment"
-              :value="column.columnName"
+              :value="column.id"
             />
           </el-select>
         </el-form-item>
       </el-col>
+      <el-col :span="12">
+        <el-form-item prop="subJoinMany">
+          <template #label>
+            <span>
+              关联关系
+              <el-tooltip content="主表与子表的关联关系" placement="top">
+                <Icon icon="ep:question-filled" />
+              </el-tooltip>
+            </span>
+          </template>
+          <el-radio-group v-model="formData.subJoinMany" placeholder="请选择">
+            <el-radio :label="true">一对多</el-radio>
+            <el-radio :label="false">一对一</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-col>
     </el-row>
   </el-form>
 </template>
@@ -305,6 +325,10 @@ const props = defineProps({
   table: {
     type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
     default: () => null
+  },
+  columns: {
+    type: Array as unknown as PropType<CodegenApi.CodegenColumnVO[]>,
+    default: () => null
   }
 })
 
@@ -323,9 +347,10 @@ const formData = ref({
   treeParentCode: '',
   treeName: '',
   tplCategory: '',
-  subTableName: '',
-  subTableFkName: '',
-  genType: ''
+  genType: '',
+  masterTableId: undefined,
+  subJoinColumnId: undefined,
+  subJoinMany: undefined
 })
 
 const rules = reactive({
@@ -336,41 +361,27 @@ const rules = reactive({
   businessName: [required],
   businessPackage: [required],
   className: [required],
-  classComment: [required]
+  classComment: [required],
+  masterTableId: [required],
+  subJoinColumnId: [required],
+  subJoinMany: [required]
 })
 
-const tables = ref([])
-const subColumns = ref([])
+const tables = ref([]) // 表定义列表
 const menus = ref<any[]>([])
 const menuTreeProps = {
   label: 'name'
 }
 
-/** 选择子表名触发 */
-const subSelectChange = () => {
-  formData.value.subTableFkName = ''
-}
-
-/** 选择生成模板触发 */
-const tplSelectChange = (value) => {
-  if (value !== 1) {
-    // TODO 芋艿:暂时不考虑支持树形结构
-    message.error(
-      '暂时不考虑支持【树形】和【主子表】的代码生成。原因是:导致 vm 模板过于复杂,不利于胖友二次开发'
-    )
-    return false
-  }
-  if (value !== 'sub') {
-    formData.value.subTableName = ''
-    formData.value.subTableFkName = ''
-  }
-}
-
 watch(
   () => props.table,
-  (table) => {
+  async (table) => {
     if (!table) return
     formData.value = table as any
+    // 加载表列表
+    if (table.dataSourceConfigId >= 0) {
+      tables.value = await CodegenApi.getCodegenTableList(formData.value.dataSourceConfigId)
+    }
   },
   {
     deep: true,
@@ -380,6 +391,7 @@ watch(
 
 onMounted(async () => {
   try {
+    // 加载菜单
     const resp = await MenuApi.getSimpleMenusList()
     menus.value = handleTree(resp)
   } catch {}
diff --git a/src/views/infra/demo12/Demo12StudentForm.vue b/src/views/infra/demo12/Demo12StudentForm.vue
index d3cb482d..af4e2733 100644
--- a/src/views/infra/demo12/Demo12StudentForm.vue
+++ b/src/views/infra/demo12/Demo12StudentForm.vue
@@ -42,7 +42,7 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="头像">
+      <el-form-item label="头像" prop="avatar">
         <UploadImg v-model="formData.avatar" />
       </el-form-item>
       <el-form-item label="附件" prop="video">
diff --git a/src/views/infra/demo12/components/Demo12StudentContactForm.vue b/src/views/infra/demo12/components/Demo12StudentContactForm.vue
index d71f6146..e246678b 100644
--- a/src/views/infra/demo12/components/Demo12StudentContactForm.vue
+++ b/src/views/infra/demo12/components/Demo12StudentContactForm.vue
@@ -1,5 +1,155 @@
 <template>
-  <div>123</div>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <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
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
 </template>
 <script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo12StudentApi from '@/api/infra/demo12'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, studentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.studentId = studentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo12StudentApi.getDemo12StudentContact(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await Demo12StudentApi.createDemo12StudentContact(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo12StudentApi.updateDemo12StudentContact(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
 </script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentContactList.vue b/src/views/infra/demo12/components/Demo12StudentContactList.vue
index 65ed9df1..b9b1e717 100644
--- a/src/views/infra/demo12/components/Demo12StudentContactList.vue
+++ b/src/views/infra/demo12/components/Demo12StudentContactList.vue
@@ -1,6 +1,14 @@
 <template>
   <!-- 列表 -->
   <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['infra:demo12-student:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="学生编号" align="center" prop="studentId" />
@@ -33,31 +41,108 @@
         :formatter="dateFormatter"
         width="180px"
       />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo12-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo12-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
     </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
   </ContentWrap>
+
+    <!-- 表单弹窗:添加/修改 -->
+    <Demo12StudentContactForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
 import { DICT_TYPE } from '@/utils/dict'
 import * as Demo12StudentApi from '@/api/infra/demo12'
+import Demo12StudentContactForm from './Demo12StudentContactForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
 const props = defineProps<{
   studentId: undefined // 学生编号(主表的关联字段)
 }>()
-const loading = ref(true) // 列表的加载中
+const loading = ref(false) // 列表的加载中
 const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    list.value = await Demo12StudentApi.getDemo12StudentContactListByStudentId(studentId.props)
+    const data = await Demo12StudentApi.getDemo12StudentContactPage(queryParams)
+    list.value = data.list
+    total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
-/** 初始化 **/
-onMounted(() => {
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
   getList()
-})
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.studentId) {
+    message.error('请选择一个学生')
+    return
+  }
+  formRef.value.open(type, id, props.studentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo12StudentApi.deleteDemo12StudentContact(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
 </script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue b/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
index d71f6146..92e2b373 100644
--- a/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
+++ b/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
@@ -1,5 +1,155 @@
 <template>
-  <div>123</div>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <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
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
 </template>
 <script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo12StudentApi from '@/api/infra/demo12'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, studentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.studentId = studentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo12StudentApi.getDemo12StudentTeacher(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await Demo12StudentApi.createDemo12StudentTeacher(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo12StudentApi.updateDemo12StudentTeacher(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
 </script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentTeacherList.vue b/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
index 55f6845b..d0594534 100644
--- a/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
+++ b/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
@@ -1,6 +1,14 @@
 <template>
   <!-- 列表 -->
   <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['infra:demo12-student:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="学生编号" align="center" prop="studentId" />
@@ -33,35 +41,108 @@
         :formatter="dateFormatter"
         width="180px"
       />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo12-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo12-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
     </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
   </ContentWrap>
+
+    <!-- 表单弹窗:添加/修改 -->
+    <Demo12StudentTeacherForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
 import { DICT_TYPE } from '@/utils/dict'
 import * as Demo12StudentApi from '@/api/infra/demo12'
+import Demo12StudentTeacherForm from './Demo12StudentTeacherForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
 const props = defineProps<{
   studentId: undefined // 学生编号(主表的关联字段)
 }>()
-const loading = ref(true) // 列表的加载中
+const loading = ref(false) // 列表的加载中
 const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    const data = await Demo12StudentApi.getDemo12StudentTeacherByStudentId(studentId.props)
-    if (!data) {
-      return
-    }
-    list.value.push(data)
+    const data = await Demo12StudentApi.getDemo12StudentTeacherPage(queryParams)
+    list.value = data.list
+    total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
-/** 初始化 **/
-onMounted(() => {
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
   getList()
-})
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.studentId) {
+    message.error('请选择一个学生')
+    return
+  }
+  formRef.value.open(type, id, props.studentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo12StudentApi.deleteDemo12StudentTeacher(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
 </script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/index.vue b/src/views/infra/demo12/index.vue
index c5a82a4c..47ec7f21 100644
--- a/src/views/infra/demo12/index.vue
+++ b/src/views/infra/demo12/index.vue
@@ -94,7 +94,14 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      highlight-current-row
+      @current-change="handleCurrentChange"
+    >
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="名字" align="center" prop="name" />
       <el-table-column label="简介" align="center" prop="description" />
@@ -157,6 +164,17 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <Demo12StudentForm ref="formRef" @success="getList" />
+  <!-- 子表的列表 -->
+  <ContentWrap>
+    <el-tabs model-value="demo12StudentContact">
+      <el-tab-pane label="学生联系人" name="demo12StudentContact">
+        <Demo12StudentContactList :student-id="currentRow.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班主任" name="demo12StudentTeacher">
+        <Demo12StudentTeacherList :student-id="currentRow.id" />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
 </template>
 
 <script setup lang="ts">
@@ -165,6 +183,8 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as Demo12StudentApi from '@/api/infra/demo12'
 import Demo12StudentForm from './Demo12StudentForm.vue'
+import Demo12StudentContactList from './components/Demo12StudentContactList.vue'
+import Demo12StudentTeacherList from './components/Demo12StudentTeacherList.vue'
 
 defineOptions({ name: 'InfraDemo12Student' })
 
@@ -245,6 +265,12 @@ const handleExport = async () => {
   }
 }
 
+/** 选中行操作 */
+const currentRow = ref({}) // 选中行
+const handleCurrentChange = (row) => {
+  currentRow.value = row
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

From 40b1522ed0da116f361a17f9ba5d6ee5df021b16 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Wed, 15 Nov 2023 23:48:00 +0800
Subject: [PATCH 10/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=20crud=20=E5=8D=95=E8=A1=A8?=
 =?UTF-8?q?=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo/demo01/index.ts            | 40 +++++++++
 src/api/infra/demo01/index.ts                 | 43 ----------
 .../demo01/Demo01ContactForm.vue}             | 84 +++++++------------
 src/views/infra/{ => demo}/demo01/index.vue   | 78 +++++------------
 4 files changed, 92 insertions(+), 153 deletions(-)
 create mode 100644 src/api/infra/demo/demo01/index.ts
 delete mode 100644 src/api/infra/demo01/index.ts
 rename src/views/infra/{demo01/Demo01StudentForm.vue => demo/demo01/Demo01ContactForm.vue} (62%)
 rename src/views/infra/{ => demo}/demo01/index.vue (72%)

diff --git a/src/api/infra/demo/demo01/index.ts b/src/api/infra/demo/demo01/index.ts
new file mode 100644
index 00000000..1a4b01ca
--- /dev/null
+++ b/src/api/infra/demo/demo01/index.ts
@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+export interface Demo01ContactVO {
+  id: number
+  name: string
+  sex: number
+  birthday: Date
+  description: string
+  avatar: string
+}
+
+// 查询示例联系人分页
+export const getDemo01ContactPage = async (params) => {
+  return await request.get({ url: `/infra/demo01-contact/page`, params })
+}
+
+// 查询示例联系人详情
+export const getDemo01Contact = async (id: number) => {
+  return await request.get({ url: `/infra/demo01-contact/get?id=` + id })
+}
+
+// 新增示例联系人
+export const createDemo01Contact = async (data: Demo01ContactVO) => {
+  return await request.post({ url: `/infra/demo01-contact/create`, data })
+}
+
+// 修改示例联系人
+export const updateDemo01Contact = async (data: Demo01ContactVO) => {
+  return await request.put({ url: `/infra/demo01-contact/update`, data })
+}
+
+// 删除示例联系人
+export const deleteDemo01Contact = async (id: number) => {
+  return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id })
+}
+
+// 导出示例联系人 Excel
+export const exportDemo01Contact = async (params) => {
+  return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
+}
\ No newline at end of file
diff --git a/src/api/infra/demo01/index.ts b/src/api/infra/demo01/index.ts
deleted file mode 100644
index 0154bc29..00000000
--- a/src/api/infra/demo01/index.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import request from '@/config/axios'
-
-export interface Demo01StudentVO {
-  id: number
-  name: string
-  description: string
-  birthday: Date
-  sex: number
-  enabled: boolean
-  avatar: string
-  video: string
-  memo: string
-}
-
-// 查询学生列表
-export const getDemo01StudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo01-student/page`, params })
-}
-
-// 查询学生详情
-export const getDemo01Student = async (id: number) => {
-  return await request.get({ url: `/infra/demo01-student/get?id=` + id })
-}
-
-// 新增学生
-export const createDemo01Student = async (data: Demo01StudentVO) => {
-  return await request.post({ url: `/infra/demo01-student/create`, data })
-}
-
-// 修改学生
-export const updateDemo01Student = async (data: Demo01StudentVO) => {
-  return await request.put({ url: `/infra/demo01-student/update`, data })
-}
-
-// 删除学生
-export const deleteDemo01Student = async (id: number) => {
-  return await request.delete({ url: `/infra/demo01-student/delete?id=` + id })
-}
-
-// 导出学生 Excel
-export const exportDemo01Student = async (params) => {
-  return await request.download({ url: `/infra/demo01-student/export-excel`, params })
-}
\ No newline at end of file
diff --git a/src/views/infra/demo01/Demo01StudentForm.vue b/src/views/infra/demo/demo01/Demo01ContactForm.vue
similarity index 62%
rename from src/views/infra/demo01/Demo01StudentForm.vue
rename to src/views/infra/demo/demo01/Demo01ContactForm.vue
index dfc9823d..0452a3c0 100644
--- a/src/views/infra/demo01/Demo01StudentForm.vue
+++ b/src/views/infra/demo/demo01/Demo01ContactForm.vue
@@ -10,31 +10,10 @@
       <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
-          v-model="formData.birthday"
-          type="date"
-          value-format="x"
-          placeholder="选择出生日期"
-        />
-      </el-form-item>
       <el-form-item label="性别" prop="sex">
-        <el-select v-model="formData.sex" placeholder="请选择性别">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-radio-group v-model="formData.enabled">
+        <el-radio-group v-model="formData.sex">
           <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
             :key="dict.value"
             :label="dict.value"
           >
@@ -42,15 +21,20 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="头像">
+      <el-form-item label="出生年" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生年"
+        />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <Editor v-model="formData.description" height="150px" />
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
         <UploadImg v-model="formData.avatar" />
       </el-form-item>
-      <el-form-item label="附件" prop="video">
-        <UploadFile v-model="formData.video" />
-      </el-form-item>
-      <el-form-item label="备注" prop="memo">
-        <Editor v-model="formData.memo" height="150px" />
-      </el-form-item>
     </el-form>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
@@ -59,8 +43,8 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo01StudentApi from '@/api/infra/demo01'
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo01ContactApi from '@/api/infra/demo/demo01'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -72,21 +56,16 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
   name: undefined,
-  description: undefined,
-  birthday: undefined,
   sex: undefined,
-  enabled: undefined,
-  avatar: undefined,
-  video: undefined,
-  memo: undefined
+  birthday: undefined,
+  description: undefined,
+  avatar: undefined
 })
 const formRules = reactive({
   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+  sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生年不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
@@ -100,7 +79,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await Demo01StudentApi.getDemo01Student(id)
+      formData.value = await Demo01ContactApi.getDemo01Contact(id)
     } finally {
       formLoading.value = false
     }
@@ -116,12 +95,12 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as Demo01StudentApi.Demo01StudentVO
+    const data = formData.value as unknown as Demo01ContactApi.Demo01ContactVO
     if (formType.value === 'create') {
-      await Demo01StudentApi.createDemo01Student(data)
+      await Demo01ContactApi.createDemo01Contact(data)
       message.success(t('common.createSuccess'))
     } else {
-      await Demo01StudentApi.updateDemo01Student(data)
+      await Demo01ContactApi.updateDemo01Contact(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -137,14 +116,11 @@ const resetForm = () => {
   formData.value = {
     id: undefined,
     name: undefined,
-    description: undefined,
-    birthday: undefined,
     sex: undefined,
-    enabled: undefined,
-    avatar: undefined,
-    video: undefined,
-    memo: undefined
+    birthday: undefined,
+    description: undefined,
+    avatar: undefined
   }
   formRef.value?.resetFields()
 }
-</script>
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo01/index.vue b/src/views/infra/demo/demo01/index.vue
similarity index 72%
rename from src/views/infra/demo01/index.vue
rename to src/views/infra/demo/demo01/index.vue
index fb35b561..55751e1b 100644
--- a/src/views/infra/demo01/index.vue
+++ b/src/views/infra/demo/demo01/index.vue
@@ -17,16 +17,6 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="出生日期" prop="birthday">
-        <el-date-picker
-          v-model="queryParams.birthday"
-          value-format="YYYY-MM-DD"
-          type="date"
-          placeholder="选择出生日期"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
       <el-form-item label="性别" prop="sex">
         <el-select
           v-model="queryParams.sex"
@@ -42,21 +32,6 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-select
-          v-model="queryParams.enabled"
-          placeholder="请选择是否有效"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -75,7 +50,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['infra:demo01-student:create']"
+          v-hasPermi="['infra:demo01-contact:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -84,7 +59,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['infra:demo01-student:export']"
+          v-hasPermi="['infra:demo01-contact:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
@@ -97,26 +72,20 @@
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="名字" align="center" prop="name" />
-      <el-table-column
-        label="出生日期"
-        align="center"
-        prop="birthday"
-        :formatter="dateFormatter"
-        width="180px"
-      />
       <el-table-column label="性别" align="center" prop="sex">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
         </template>
       </el-table-column>
-      <el-table-column label="是否有效" align="center" prop="enabled">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
-        </template>
-      </el-table-column>
+      <el-table-column
+        label="出生年"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="简介" align="center" prop="description" />
       <el-table-column label="头像" align="center" prop="avatar" />
-      <el-table-column label="附件" align="center" prop="video" />
-      <el-table-column label="备注" align="center" prop="memo" />
       <el-table-column
         label="创建时间"
         align="center"
@@ -130,7 +99,7 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['infra:demo01-student:update']"
+            v-hasPermi="['infra:demo01-contact:update']"
           >
             编辑
           </el-button>
@@ -138,7 +107,7 @@
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo01-student:delete']"
+            v-hasPermi="['infra:demo01-contact:delete']"
           >
             删除
           </el-button>
@@ -155,32 +124,29 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <Demo01StudentForm ref="formRef" @success="getList" />
+  <Demo01ContactForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as Demo01StudentApi from '@/api/infra/demo01'
-import Demo01StudentForm from './Demo01StudentForm.vue'
+import * as Demo01ContactApi from '@/api/infra/demo/demo01'
+import Demo01ContactForm from './Demo01ContactForm.vue'
 
-defineOptions({ name: 'InfraDemo01Student' })
+defineOptions({ name: 'Demo01Contact' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: null,
-  birthday: null,
-  birthday: [],
   sex: null,
-  enabled: null,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
@@ -190,7 +156,7 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    const data = await Demo01StudentApi.getDemo01StudentPage(queryParams)
+    const data = await Demo01ContactApi.getDemo01ContactPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -222,7 +188,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await Demo01StudentApi.deleteDemo01Student(id)
+    await Demo01ContactApi.deleteDemo01Contact(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -236,8 +202,8 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await Demo01StudentApi.exportDemo01Student(queryParams)
-    download.excel(data, '学生.xls')
+    const data = await Demo01ContactApi.exportDemo01Contact(queryParams)
+    download.excel(data, '示例联系人.xls')
   } catch {
   } finally {
     exportLoading.value = false

From 3d0c4f442210e89062f0cf407a31f27e47984e06 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 16 Nov 2023 00:11:23 +0800
Subject: [PATCH 11/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E6=94=AF=E6=8C=81=E6=A0=91=E5=BD=A2=E8=A1=A8=E7=BB=93?=
 =?UTF-8?q?=E6=9E=84=E7=9A=84=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../codegen/components/GenerateInfoForm.vue   | 58 +++++++------------
 1 file changed, 20 insertions(+), 38 deletions(-)

diff --git a/src/views/infra/codegen/components/GenerateInfoForm.vue b/src/views/infra/codegen/components/GenerateInfoForm.vue
index 178b4e0f..a4f5ad97 100644
--- a/src/views/infra/codegen/components/GenerateInfoForm.vue
+++ b/src/views/infra/codegen/components/GenerateInfoForm.vue
@@ -182,50 +182,33 @@
       </el-col>
     </el-row>
 
-    <el-row v-show="formData.tplCategory === 'tree'">
-      <h4 class="form-header">其他信息</h4>
-      <el-col :span="12">
-        <el-form-item>
-          <template #label>
-            <span>
-              树编码字段
-              <el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
-                <Icon icon="ep:question-filled" />
-              </el-tooltip>
-            </span>
-          </template>
-          <el-select v-model="formData.treeCode" placeholder="请选择">
-            <el-option
-              v-for="(column, index) in formData.columns"
-              :key="index"
-              :label="column.columnName + ':' + column.columnComment"
-              :value="column.columnName"
-            />
-          </el-select>
-        </el-form-item>
+    <!-- 树表信息 -->
+    <el-row v-show="formData.templateType == 2">
+      <el-col :span="24">
+        <h4 class="form-header">树表信息</h4>
       </el-col>
       <el-col :span="12">
-        <el-form-item>
+        <el-form-item prop="treeParentColumnId">
           <template #label>
             <span>
-              树父编码字段
+              父编号字段
               <el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
                 <Icon icon="ep:question-filled" />
               </el-tooltip>
             </span>
           </template>
-          <el-select v-model="formData.treeParentCode" placeholder="请选择">
+          <el-select v-model="formData.treeParentColumnId" placeholder="请选择">
             <el-option
-              v-for="(column, index) in formData.columns"
+              v-for="(column, index) in props.columns"
               :key="index"
               :label="column.columnName + ':' + column.columnComment"
-              :value="column.columnName"
+              :value="column.id"
             />
           </el-select>
         </el-form-item>
       </el-col>
       <el-col :span="12">
-        <el-form-item>
+        <el-form-item prop="treeNameColumnId">
           <template #label>
             <span>
               树名称字段
@@ -234,13 +217,12 @@
               </el-tooltip>
             </span>
           </template>
-
-          <el-select v-model="formData.treeName" placeholder="请选择">
+          <el-select v-model="formData.treeNameColumnId" placeholder="请选择">
             <el-option
-              v-for="(column, index) in formData.columns"
+              v-for="(column, index) in props.columns"
               :key="index"
               :label="column.columnName + ':' + column.columnComment"
-              :value="column.columnName"
+              :value="column.id"
             />
           </el-select>
         </el-form-item>
@@ -248,7 +230,7 @@
     </el-row>
 
     <!-- 主表信息 -->
-    <el-row v-if="formData.templateType === 15">
+    <el-row v-if="formData.templateType == 15">
       <el-col :span="24">
         <h4 class="form-header">主表信息</h4>
       </el-col>
@@ -343,14 +325,12 @@ const formData = ref({
   classComment: '',
   parentMenuId: null,
   genPath: '',
-  treeCode: '',
-  treeParentCode: '',
-  treeName: '',
-  tplCategory: '',
   genType: '',
   masterTableId: undefined,
   subJoinColumnId: undefined,
-  subJoinMany: undefined
+  subJoinMany: undefined,
+  treeParentColumnId: undefined,
+  treeNameColumnId: undefined
 })
 
 const rules = reactive({
@@ -364,7 +344,9 @@ const rules = reactive({
   classComment: [required],
   masterTableId: [required],
   subJoinColumnId: [required],
-  subJoinMany: [required]
+  subJoinMany: [required],
+  treeParentColumnId: [required],
+  treeNameColumnId: [required]
 })
 
 const tables = ref([]) // 表定义列表

From bbc37613b6c1aa236752926d09c997b046f117e8 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 16 Nov 2023 20:39:12 +0800
Subject: [PATCH 12/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=20tree=20=E6=A0=91=E5=BD=A2?=
 =?UTF-8?q?=E7=9A=84=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo/demo02/index.ts            |  37 ++++
 src/api/infra/demo02/index.ts                 |  49 -----
 .../demo02/Demo02CategoryForm.vue}            |  85 ++++-----
 src/views/infra/{ => demo}/demo02/index.vue   | 105 ++++++++---
 .../infra/demo02/DemoStudentAddressForm.vue   | 126 -------------
 .../infra/demo02/DemoStudentContactForm.vue   | 177 ------------------
 .../demo02/bak/DemoStudentAddressForm.vue     |  58 ------
 7 files changed, 159 insertions(+), 478 deletions(-)
 create mode 100644 src/api/infra/demo/demo02/index.ts
 delete mode 100644 src/api/infra/demo02/index.ts
 rename src/views/infra/{demo02/DemoStudentForm.vue => demo/demo02/Demo02CategoryForm.vue} (55%)
 rename src/views/infra/{ => demo}/demo02/index.vue (51%)
 delete mode 100644 src/views/infra/demo02/DemoStudentAddressForm.vue
 delete mode 100644 src/views/infra/demo02/DemoStudentContactForm.vue
 delete mode 100644 src/views/infra/demo02/bak/DemoStudentAddressForm.vue

diff --git a/src/api/infra/demo/demo02/index.ts b/src/api/infra/demo/demo02/index.ts
new file mode 100644
index 00000000..21e45c90
--- /dev/null
+++ b/src/api/infra/demo/demo02/index.ts
@@ -0,0 +1,37 @@
+import request from '@/config/axios'
+
+export interface Demo02CategoryVO {
+  id: number
+  name: string
+  parentId: number
+}
+
+// 查询示例分类列表
+export const getDemo02CategoryList = async (params) => {
+  return await request.get({ url: `/infra/demo02-category/list`, params })
+}
+
+// 查询示例分类详情
+export const getDemo02Category = async (id: number) => {
+  return await request.get({ url: `/infra/demo02-category/get?id=` + id })
+}
+
+// 新增示例分类
+export const createDemo02Category = async (data: Demo02CategoryVO) => {
+  return await request.post({ url: `/infra/demo02-category/create`, data })
+}
+
+// 修改示例分类
+export const updateDemo02Category = async (data: Demo02CategoryVO) => {
+  return await request.put({ url: `/infra/demo02-category/update`, data })
+}
+
+// 删除示例分类
+export const deleteDemo02Category = async (id: number) => {
+  return await request.delete({ url: `/infra/demo02-category/delete?id=` + id })
+}
+
+// 导出示例分类 Excel
+export const exportDemo02Category = async (params) => {
+  return await request.download({ url: `/infra/demo02-category/export-excel`, params })
+}
\ No newline at end of file
diff --git a/src/api/infra/demo02/index.ts b/src/api/infra/demo02/index.ts
deleted file mode 100644
index 500adde5..00000000
--- a/src/api/infra/demo02/index.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import request from '@/config/axios'
-
-export interface DemoStudentVO {
-  id: number
-}
-
-// 查询学生列表
-export const getDemoStudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo-student/page`, params })
-}
-
-// 查询学生详情
-export const getDemoStudent = async (id: number) => {
-  return await request.get({ url: `/infra/demo-student/get?id=` + id })
-}
-
-// 新增学生
-export const createDemoStudent = async (data: DemoStudentVO) => {
-  return await request.post({ url: `/infra/demo-student/create`, data })
-}
-
-// 修改学生
-export const updateDemoStudent = async (data: DemoStudentVO) => {
-  return await request.put({ url: `/infra/demo-student/update`, data })
-}
-
-// 删除学生
-export const deleteDemoStudent = async (id: number) => {
-  return await request.delete({ url: `/infra/demo-student/delete?id=` + id })
-}
-
-// 导出学生 Excel
-export const exportDemoStudent = async (params) => {
-  return await request.download({ url: `/infra/demo-student/export-excel`, params })
-}
-
-// 获得学生联系人列表
-export const getDemoStudentContactListByStudentId = async (studentId) => {
-  return await request.get({
-    url: `/infra/demo-student/demo-student/list-by-student-id?studentId=` + studentId
-  })
-}
-
-// 获得学生地址
-export const getDemoStudentAddressByStudentId = async (studentId) => {
-  return await request.get({
-    url: `/infra/demo-student/demo-student/get-by-student-id?studentId=` + studentId
-  })
-}
diff --git a/src/views/infra/demo02/DemoStudentForm.vue b/src/views/infra/demo/demo02/Demo02CategoryForm.vue
similarity index 55%
rename from src/views/infra/demo02/DemoStudentForm.vue
rename to src/views/infra/demo/demo02/Demo02CategoryForm.vue
index e9454c0f..9002d5ee 100644
--- a/src/views/infra/demo02/DemoStudentForm.vue
+++ b/src/views/infra/demo/demo02/Demo02CategoryForm.vue
@@ -6,16 +6,21 @@
       :rules="formRules"
       label-width="100px"
       v-loading="formLoading"
-    />
-    <!-- 子表的表单 -->
-    <el-tabs v-model="subTabsName">
-      <el-tab-pane label="学生联系人" name="demoStudentContact">
-        <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-      <el-tab-pane label="学生地址" name="demoStudentAddress">
-        <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-    </el-tabs>
+    >
+      <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
+          v-model="formData.parentId"
+          :data="demo02CategoryTree"
+          :props="defaultProps"
+          check-strictly
+          default-expand-all
+          placeholder="请选择父级编号"
+        />
+      </el-form-item>
+    </el-form>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -23,9 +28,8 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentContactForm from './DemoStudentContactForm.vue'
-import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
+import * as Demo02CategoryApi from '@/api/infra/demo/demo02'
+import { defaultProps, handleTree } from '@/utils/tree'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -35,15 +39,16 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
-  id: undefined
+  id: undefined,
+  name: undefined,
+  parentId: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  parentId: [{ required: true, message: '父级编号不能为空', trigger: 'blur' }]
 })
-const formRules = reactive({})
 const formRef = ref() // 表单 Ref
-
-/** 子表的表单 */
-const subTabsName = ref('demoStudentContact')
-const demoStudentContactFormRef = ref()
-const demoStudentAddressFormRef = ref()
+const demo02CategoryTree = ref() // 树形结构
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -55,11 +60,12 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await DemoStudentApi.getDemoStudent(id)
+      formData.value = await Demo02CategoryApi.getDemo02Category(id)
     } finally {
       formLoading.value = false
     }
   }
+  await getDemo02CategoryTree()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -68,31 +74,15 @@ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成
 const submitForm = async () => {
   // 校验表单
   await formRef.value.validate()
-  // 校验子表单
-  try {
-    await demoStudentContactFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'demoStudentContact'
-    return
-  }
-  try {
-    await demoStudentAddressFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'demoStudentAddress'
-    return
-  }
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
-    // 拼接子表的数据
-    data.demoStudentContacts = demoStudentContactFormRef.value.getData()
-    data.demoStudentAddress = demoStudentAddressFormRef.value.getData()
+    const data = formData.value as unknown as Demo02CategoryApi.Demo02CategoryVO
     if (formType.value === 'create') {
-      await DemoStudentApi.createDemoStudent(data)
+      await Demo02CategoryApi.createDemo02Category(data)
       message.success(t('common.createSuccess'))
     } else {
-      await DemoStudentApi.updateDemoStudent(data)
+      await Demo02CategoryApi.updateDemo02Category(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -106,8 +96,19 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
-    id: undefined
+    id: undefined,
+    name: undefined,
+    parentId: undefined
   }
   formRef.value?.resetFields()
 }
-</script>
+
+/** 获得示例分类树 */
+const getDemo02CategoryTree = async () => {
+  demo02CategoryTree.value = []
+  const data = await Demo02CategoryApi.getDemo02CategoryList()
+  const root: Tree = { id: 0, name: '顶级示例分类', children: [] }
+  root.children = handleTree(data, 'id', 'parentId')
+  demo02CategoryTree.value.push(root)
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo02/index.vue b/src/views/infra/demo/demo02/index.vue
similarity index 51%
rename from src/views/infra/demo02/index.vue
rename to src/views/infra/demo/demo02/index.vue
index 89fd656b..e46c77dc 100644
--- a/src/views/infra/demo02/index.vue
+++ b/src/views/infra/demo/demo02/index.vue
@@ -8,10 +8,35 @@
       :inline="true"
       label-width="68px"
     >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button type="primary" plain @click="openForm('create')">
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo02-category:create']"
+        >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
@@ -19,28 +44,52 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['infra:demo-student:export']"
+          v-hasPermi="['infra:demo02-category:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      v-if="refreshTable"
+    >
       <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
       <el-table-column label="操作" align="center">
         <template #default="scope">
-          <el-button link type="primary" @click="openForm('update', scope.row.id)">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo02-category:update']"
+          >
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo-student:delete']"
+            v-hasPermi="['infra:demo02-category:delete']"
           >
             删除
           </el-button>
@@ -57,25 +106,27 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <DemoStudentForm ref="formRef" @success="getList" />
+  <Demo02CategoryForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
 import download from '@/utils/download'
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentForm from './DemoStudentForm.vue'
+import * as Demo02CategoryApi from '@/api/infra/demo/demo02'
+import Demo02CategoryForm from './Demo02CategoryForm.vue'
 
-defineOptions({ name: 'InfraDemoStudent' })
+defineOptions({ name: 'Demo02Category' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
 const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10
+  name: null,
+  parentId: null,
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -84,13 +135,8 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: 1
-      }
-    ]
-    total.value = 10
+    const data = await Demo02CategoryApi.getDemo02CategoryList(queryParams)
+    list.value = handleTree(data, 'id', 'parentId')
   } finally {
     loading.value = false
   }
@@ -110,10 +156,7 @@ const resetQuery = () => {
 
 /** 添加/修改操作 */
 const formRef = ref()
-// const demoStudentContactFormRef = ref()
 const openForm = (type: string, id?: number) => {
-  // console.log(demoStudentContactFormRef, 'xx demoStudentContactFormRef xx')
-  // demoStudentContactFormRef.value.validate()
   formRef.value.open(type, id)
 }
 
@@ -123,7 +166,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await DemoStudentApi.deleteDemoStudent(id)
+    await Demo02CategoryApi.deleteDemo02Category(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -137,16 +180,26 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await DemoStudentApi.exportDemoStudent(queryParams)
-    download.excel(data, '学生.xls')
+    const data = await Demo02CategoryApi.exportDemo02Category(queryParams)
+    download.excel(data, '示例分类.xls')
   } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
+/** 展开/折叠操作 */
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const toggleExpandAll = async () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  await nextTick()
+  refreshTable.value = true
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()
 })
-</script>
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo02/DemoStudentAddressForm.vue b/src/views/infra/demo02/DemoStudentAddressForm.vue
deleted file mode 100644
index 6d031406..00000000
--- a/src/views/infra/demo02/DemoStudentAddressForm.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    label-width="100px"
-    v-loading="formLoading"
-  >
-    <el-form-item label="名字" prop="name">
-      <el-input v-model="formData.name" placeholder="请输入名字" />
-    </el-form-item>
-    <el-form-item label="个人简介">
-      <Editor v-model="formData.description" height="150px" />
-    </el-form-item>
-    <el-form-item label="性别 1" prop="sex1">
-      <el-select v-model="formData.sex1" placeholder="请选择性别 1">
-        <el-option
-          v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SEX1)"
-          :key="dict.value"
-          :label="dict.label"
-          :value="dict.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="性别 2" prop="sex2">
-      <el-checkbox-group v-model="formData.sex2">
-        <el-checkbox
-          v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SEX2)"
-          :key="dict.value"
-          :label="dict.value"
-        >
-          {{ dict.label }}
-        </el-checkbox>
-      </el-checkbox-group>
-    </el-form-item>
-    <el-form-item label="性别 3" prop="sex3">
-      <el-radio-group v-model="formData.sex3">
-        <el-radio
-          v-for="dict in getBoolDictOptions(DICT_TYPE.SYSTEM_SEX3)"
-          :key="dict.value"
-          :label="dict.value"
-        >
-          {{ dict.label }}
-        </el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item label="出生日期" prop="birthday">
-      <el-date-picker
-        v-model="formData.birthday"
-        type="date"
-        value-format="x"
-        placeholder="选择出生日期"
-      />
-    </el-form-item>
-    <el-form-item label="备注" prop="memo">
-      <el-input v-model="formData.memo" type="textarea" placeholder="请输入备注" />
-    </el-form-item>
-  </el-form>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as DemoStudentApi from '@/api/infra/demo02'
-
-const props = defineProps<{
-  studentId: undefined // 学生编号(主表的关联字段)
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref([])
-const formRules = reactive({
-  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  video: [{ required: true, message: '视频不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '个人简介不能为空', trigger: 'blur' }],
-  sex1: [{ required: true, message: '性别 1不能为空', trigger: 'change' }],
-  sex2: [{ required: true, message: '性别 2不能为空', trigger: 'blur' }],
-  sex3: [{ required: true, message: '性别 3不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  async (val) => {
-    // 情况一:val 为空,说明是新增,则置空
-    if (!val) {
-      formData.value = {
-        id: undefined,
-        studentId: undefined,
-        name: undefined,
-        avatar: undefined,
-        video: undefined,
-        description: undefined,
-        sex1: undefined,
-        sex2: [],
-        sex3: undefined,
-        birthday: undefined,
-        memo: undefined
-      }
-      return
-    }
-    // 情况二:val 非空,说明是修改,则加载数据
-    try {
-      formLoading.value = true
-      formData.value = await DemoStudentApi.getDemoStudentAddressByStudentId(val)
-    } finally {
-      formLoading.value = false
-    }
-  },
-  { immediate: true }
-)
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 */
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>
diff --git a/src/views/infra/demo02/DemoStudentContactForm.vue b/src/views/infra/demo02/DemoStudentContactForm.vue
deleted file mode 100644
index 5f173f14..00000000
--- a/src/views/infra/demo02/DemoStudentContactForm.vue
+++ /dev/null
@@ -1,177 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    v-loading="formLoading"
-    label-width="0px"
-    :inline-message="true"
-  >
-    <el-table :data="formData" class="-mt-10px">
-      <el-table-column label="序号" type="index" width="100" />
-      <el-table-column label="名字" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
-            <el-input v-model="row.name" placeholder="请输入名字" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="个人简介" width="400">
-        <template #default="{ row, $index }">
-          <el-form-item
-            :prop="`${$index}.description`"
-            :rules="formRules.description"
-            class="mb-0px!"
-          >
-            <Editor v-model="row.description" height="150px" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="性别 1" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.sex1`" :rules="formRules.sex1" class="mb-0px!">
-            <el-select v-model="row.sex1" placeholder="请选择性别 1">
-              <el-option
-                v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SEX1)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="性别 2" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.sex2`" :rules="formRules.sex2" class="mb-0px!">
-            <el-checkbox-group v-model="row.sex2">
-              <el-checkbox
-                v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SEX2)"
-                :key="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-checkbox>
-            </el-checkbox-group>
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="性别 3" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.sex3`" :rules="formRules.sex3" class="mb-0px!">
-            <el-radio-group v-model="row.sex3">
-              <el-radio
-                v-for="dict in getBoolDictOptions(DICT_TYPE.SYSTEM_SEX3)"
-                :key="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="出生日期" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
-            <el-date-picker
-              v-model="row.birthday"
-              type="date"
-              value-format="x"
-              placeholder="选择出生日期"
-            />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="备注" width="200">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
-            <el-input v-model="row.memo" type="textarea" placeholder="请输入备注" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-    </el-table>
-  </el-form>
-  <el-row justify="center" class="mt-3">
-    <el-button @click="handleAdd" round>+ 添加联系人</el-button>
-  </el-row>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as DemoStudentApi from '@/api/infra/demo02'
-
-const props = defineProps<{
-  studentId: undefined // 学生编号(主表的关联字段)
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref([])
-const formRules = reactive({
-  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  video: [{ required: true, message: '视频不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '个人简介不能为空', trigger: 'blur' }],
-  sex1: [{ required: true, message: '性别 1不能为空', trigger: 'change' }],
-  sex2: [{ required: true, message: '性别 2不能为空', trigger: 'blur' }],
-  sex3: [{ required: true, message: '性别 3不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  async (val) => {
-    // 情况一:val 为空,说明是新增,则置空
-    if (!val) {
-      formData.value = []
-      return
-    }
-    // 情况二:val 非空,说明是修改,则加载数据
-    try {
-      formLoading.value = true
-      formData.value = await DemoStudentApi.getDemoStudentContactListByStudentId(val)
-    } finally {
-      formLoading.value = false
-    }
-  },
-  { immediate: true }
-)
-
-/** 新增按钮操作 */
-const handleAdd = () => {
-  const row = {
-    id: undefined,
-    studentId: undefined,
-    name: undefined,
-    avatar: undefined,
-    video: undefined,
-    description: undefined,
-    sex1: undefined,
-    sex2: [],
-    sex3: undefined,
-    birthday: undefined,
-    memo: undefined
-  }
-  row.studentId = props.studentId
-  formData.value.push(row)
-}
-
-/** 删除按钮操作 */
-const handleDelete = (index) => {
-  formData.value.splice(index, 1)
-}
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 */
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>
diff --git a/src/views/infra/demo02/bak/DemoStudentAddressForm.vue b/src/views/infra/demo02/bak/DemoStudentAddressForm.vue
deleted file mode 100644
index af4cd8f1..00000000
--- a/src/views/infra/demo02/bak/DemoStudentAddressForm.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    label-width="100px"
-    v-loading="formLoading"
-  >
-    <el-form-item label="子字段 1" prop="field1">
-      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-    </el-form-item>
-    <el-form-item label="子字段 2" prop="field2">
-      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
-    </el-form-item>
-    <el-form-item label="子字段 3" prop="field3">
-      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
-    </el-form-item>
-  </el-form>
-</template>
-<script setup lang="ts">
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({})
-const formRules = reactive({
-  field1: [required]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  (val) => {
-    if (val) {
-      formData.value = {
-        field2: '番茄',
-        field3: '西瓜'
-      }
-    } else {
-      formData.value = {}
-    }
-  },
-  { immediate: true }
-)
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 **/
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>

From 9aca49c1ffb1344f5416d793e6c9edc56179ffab Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 16 Nov 2023 23:24:03 +0800
Subject: [PATCH 13/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=B8=BB=E5=AD=90=E8=A1=A8=20normal?=
 =?UTF-8?q?=20=E6=A8=A1=E5=BC=8F=E7=9A=84=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo/demo03/normal/index.ts     |  53 ++++++
 .../codegen/components/GenerateInfoForm.vue   |   2 +-
 .../demo/demo03/normal/Demo03StudentForm.vue  | 153 ++++++++++++++++++
 .../normal/components/Demo03CourseForm.vue}   |  61 ++++---
 .../normal/components/Demo03GradeForm.vue     |  72 +++++++++
 .../{demo03 => demo/demo03/normal}/index.vue  | 133 +++++++++------
 .../infra/demo03/DemoStudentAddressForm.vue   |  58 -------
 .../infra/demo03/DemoStudentAddressList.vue   |  38 -----
 .../infra/demo03/DemoStudentContactList.vue   |  38 -----
 src/views/infra/demo03/DemoStudentForm.vue    | 132 ---------------
 10 files changed, 402 insertions(+), 338 deletions(-)
 create mode 100644 src/api/infra/demo/demo03/normal/index.ts
 create mode 100644 src/views/infra/demo/demo03/normal/Demo03StudentForm.vue
 rename src/views/infra/{demo03/DemoStudentContactForm.vue => demo/demo03/normal/components/Demo03CourseForm.vue} (52%)
 create mode 100644 src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue
 rename src/views/infra/{demo03 => demo/demo03/normal}/index.vue (50%)
 delete mode 100644 src/views/infra/demo03/DemoStudentAddressForm.vue
 delete mode 100644 src/views/infra/demo03/DemoStudentAddressList.vue
 delete mode 100644 src/views/infra/demo03/DemoStudentContactList.vue
 delete mode 100644 src/views/infra/demo03/DemoStudentForm.vue

diff --git a/src/api/infra/demo/demo03/normal/index.ts b/src/api/infra/demo/demo03/normal/index.ts
new file mode 100644
index 00000000..f15ee1dc
--- /dev/null
+++ b/src/api/infra/demo/demo03/normal/index.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+export interface Demo03StudentVO {
+  id: number
+  name: string
+  sex: number
+  birthday: Date
+  description: string
+}
+
+// 查询学生分页
+export const getDemo03StudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo03-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemo03Student = async (id: number) => {
+  return await request.get({ url: `/infra/demo03-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemo03Student = async (data: Demo03StudentVO) => {
+  return await request.post({ url: `/infra/demo03-student/create`, data })
+}
+
+// 修改学生
+export const updateDemo03Student = async (data: Demo03StudentVO) => {
+  return await request.put({ url: `/infra/demo03-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemo03Student = async (id: number) => {
+  return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemo03Student = async (params) => {
+  return await request.download({ url: `/infra/demo03-student/export-excel`, params })
+}
+
+// ==================== 子表(学生课程) ====================
+
+// 获得学生课程列表
+export const getDemo03CourseListByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
+}
+
+// ==================== 子表(学生班级) ====================
+
+// 获得学生班级
+export const getDemo03GradeByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
+}
\ No newline at end of file
diff --git a/src/views/infra/codegen/components/GenerateInfoForm.vue b/src/views/infra/codegen/components/GenerateInfoForm.vue
index a4f5ad97..d2a01cc0 100644
--- a/src/views/infra/codegen/components/GenerateInfoForm.vue
+++ b/src/views/infra/codegen/components/GenerateInfoForm.vue
@@ -183,7 +183,7 @@
     </el-row>
 
     <!-- 树表信息 -->
-    <el-row v-show="formData.templateType == 2">
+    <el-row v-if="formData.templateType == 2">
       <el-col :span="24">
         <h4 class="form-header">树表信息</h4>
       </el-col>
diff --git a/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue b/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue
new file mode 100644
index 00000000..00508228
--- /dev/null
+++ b/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue
@@ -0,0 +1,153 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-radio-group v-model="formData.sex">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <Editor v-model="formData.description" height="150px" />
+      </el-form-item>
+    </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="学生课程" name="demo03Course">
+        <Demo03CourseForm ref="demo03CourseFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班级" name="demo03Grade">
+        <Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/normal'
+import Demo03CourseForm from './components/Demo03CourseForm.vue'
+import Demo03GradeForm from './components/Demo03GradeForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  sex: undefined,
+  birthday: undefined,
+  description: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 子表的表单 */
+const subTabsName = ref('demo03Course')
+const demo03CourseFormRef = ref()
+const demo03GradeFormRef = ref()
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo03StudentApi.getDemo03Student(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demo03CourseFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo03Course'
+    return
+  }
+  try {
+    await demo03GradeFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo03Grade'
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as Demo03StudentApi.Demo03StudentVO
+    // 拼接子表的数据
+    data.demo03Courses = demo03CourseFormRef.value.getData()
+    data.demo03Grade = demo03GradeFormRef.value.getData()
+    if (formType.value === 'create') {
+      await Demo03StudentApi.createDemo03Student(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo03StudentApi.updateDemo03Student(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    sex: undefined,
+    birthday: undefined,
+    description: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo03/DemoStudentContactForm.vue b/src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue
similarity index 52%
rename from src/views/infra/demo03/DemoStudentContactForm.vue
rename to src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue
index 50c44263..b6f58572 100644
--- a/src/views/infra/demo03/DemoStudentContactForm.vue
+++ b/src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue
@@ -3,23 +3,23 @@
     ref="formRef"
     :model="formData"
     :rules="formRules"
-    label-width="0px"
     v-loading="formLoading"
+    label-width="0px"
     :inline-message="true"
   >
     <el-table :data="formData" class="-mt-10px">
       <el-table-column label="序号" type="index" width="100" />
-      <el-table-column label="名字" prop="name" width="300">
-        <template #default="row">
-          <el-form-item class="mb-0px!">
+       <el-table-column label="名字" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
             <el-input v-model="row.name" placeholder="请输入名字" />
           </el-form-item>
         </template>
       </el-table-column>
-      <el-table-column label="手机号码">
+      <el-table-column label="分数" min-width="150">
         <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.mobile`" :rules="formRules.mobile" class="mb-0px!">
-            <el-input type="number" placeholder="输入手机号码" v-model="row.mobile" />
+          <el-form-item :prop="`${$index}.score`" :rules="formRules.score" class="mb-0px!">
+            <el-input v-model="row.score" placeholder="请输入分数" />
           </el-form-item>
         </template>
       </el-table-column>
@@ -31,33 +31,39 @@
     </el-table>
   </el-form>
   <el-row justify="center" class="mt-3">
-    <el-button @click="handleAdd" round>+ 添加联系人</el-button>
+    <el-button @click="handleAdd" round>+ 添加学生课程</el-button>
   </el-row>
 </template>
 <script setup lang="ts">
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/normal'
+
 const props = defineProps<{
-  studentId: undefined // 学生编号
+  studentId: undefined // 学生编号(主表的关联字段)
 }>()
 const formLoading = ref(false) // 表单的加载中
 const formData = ref([])
 const formRules = reactive({
-  mobile: [required]
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  score: [{ required: true, message: '分数不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
 /** 监听主表的关联字段的变化,加载对应的子表数据 */
 watch(
   () => props.studentId,
-  (val) => {
-    if (val) {
-      formData.value = [
-        {
-          name: '芋艿',
-          mobile: '15601691300'
-        }
-      ]
-    } else {
-      formData.value = []
+  async (val) => {
+    // 1. 重置表单
+    formData.value = []
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      formData.value = await Demo03StudentApi.getDemo03CourseListByStudentId(val)
+    } finally {
+      formLoading.value = false
     }
   },
   { immediate: true }
@@ -65,9 +71,14 @@ watch(
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-  formData.value.push({
-    name: '土豆'
-  })
+  const row = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    score: undefined
+  }
+  row.studentId = props.studentId
+  formData.value.push(row)
 }
 
 /** 删除按钮操作 */
@@ -80,10 +91,10 @@ const validate = () => {
   return formRef.value.validate()
 }
 
-/** 表单值 **/
+/** 表单值 */
 const getData = () => {
   return formData.value
 }
 
 defineExpose({ validate, getData })
-</script>
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue b/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue
new file mode 100644
index 00000000..12653b6c
--- /dev/null
+++ b/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue
@@ -0,0 +1,72 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+     <el-form-item label="名字" prop="name">
+      <el-input v-model="formData.name" placeholder="请输入名字" />
+    </el-form-item>
+    <el-form-item label="班主任" prop="teacher">
+      <el-input v-model="formData.teacher" placeholder="请输入班主任" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/normal'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = {
+      id: undefined,
+      studentId: undefined,
+      name: undefined,
+      teacher: undefined,
+    }
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      const data = await Demo03StudentApi.getDemo03GradeByStudentId(val)
+      if (!data) {
+        return
+      }
+      formData.value = data
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo03/index.vue b/src/views/infra/demo/demo03/normal/index.vue
similarity index 50%
rename from src/views/infra/demo03/index.vue
rename to src/views/infra/demo/demo03/normal/index.vue
index afd5cbfc..52029107 100644
--- a/src/views/infra/demo03/index.vue
+++ b/src/views/infra/demo/demo03/normal/index.vue
@@ -8,10 +8,50 @@
       :inline="true"
       label-width="68px"
     >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button type="primary" plain @click="openForm('create')">
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo03-student:create']"
+        >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
@@ -19,7 +59,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['infra:demo-student:export']"
+          v-hasPermi="['infra:demo03-student:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
@@ -29,37 +69,44 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-table
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="true"
-      row-key="id"
-    >
-      <el-table-column type="expand">
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="性别" align="center" prop="sex">
         <template #default="scope">
-          <!-- 子表的表单 -->
-          <el-tabs model-value="DemoStudentContact">
-            <el-tab-pane label="联系人信息" name="DemoStudentContact">
-              <DemoStudentContactList :student-id="scope.row.id" />
-            </el-tab-pane>
-            <el-tab-pane label="地址信息" name="DemoStudentAddress">
-              <DemoStudentAddressList :student-id="scope.row.id" />
-            </el-tab-pane>
-          </el-tabs>
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
         </template>
       </el-table-column>
-      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
       <el-table-column label="操作" align="center">
         <template #default="scope">
-          <el-button link type="primary" @click="openForm('update', scope.row.id)">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
+          >
             编辑
           </el-button>
           <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo-student:delete']"
+            v-hasPermi="['infra:demo03-student:delete']"
           >
             删除
           </el-button>
@@ -76,27 +123,31 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <DemoStudentForm ref="formRef" @success="getList" />
+  <Demo03StudentForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentForm from './DemoStudentForm.vue'
-import DemoStudentContactList from './DemoStudentContactList.vue'
-import DemoStudentAddressList from './DemoStudentAddressList.vue'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/normal'
+import Demo03StudentForm from './Demo03StudentForm.vue'
 
-defineOptions({ name: 'InfraDemoStudent' })
+defineOptions({ name: 'Demo03Student' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
-  pageSize: 10
+  pageSize: 10,
+  name: null,
+  sex: null,
+  description: null,
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -105,16 +156,9 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: 1
-      },
-      {
-        id: 10
-      }
-    ]
-    total.value = 10
+    const data = await Demo03StudentApi.getDemo03StudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
   } finally {
     loading.value = false
   }
@@ -134,10 +178,7 @@ const resetQuery = () => {
 
 /** 添加/修改操作 */
 const formRef = ref()
-// const demoStudentContactFormRef = ref()
 const openForm = (type: string, id?: number) => {
-  // console.log(demoStudentContactFormRef, 'xx demoStudentContactFormRef xx')
-  // demoStudentContactFormRef.value.validate()
   formRef.value.open(type, id)
 }
 
@@ -147,7 +188,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await DemoStudentApi.deleteDemoStudent(id)
+    await Demo03StudentApi.deleteDemo03Student(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -161,7 +202,7 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await DemoStudentApi.exportDemoStudent(queryParams)
+    const data = await Demo03StudentApi.exportDemo03Student(queryParams)
     download.excel(data, '学生.xls')
   } catch {
   } finally {
@@ -173,4 +214,4 @@ const handleExport = async () => {
 onMounted(() => {
   getList()
 })
-</script>
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo03/DemoStudentAddressForm.vue b/src/views/infra/demo03/DemoStudentAddressForm.vue
deleted file mode 100644
index af4cd8f1..00000000
--- a/src/views/infra/demo03/DemoStudentAddressForm.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    label-width="100px"
-    v-loading="formLoading"
-  >
-    <el-form-item label="子字段 1" prop="field1">
-      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-    </el-form-item>
-    <el-form-item label="子字段 2" prop="field2">
-      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
-    </el-form-item>
-    <el-form-item label="子字段 3" prop="field3">
-      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
-    </el-form-item>
-  </el-form>
-</template>
-<script setup lang="ts">
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({})
-const formRules = reactive({
-  field1: [required]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  (val) => {
-    if (val) {
-      formData.value = {
-        field2: '番茄',
-        field3: '西瓜'
-      }
-    } else {
-      formData.value = {}
-    }
-  },
-  { immediate: true }
-)
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 **/
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>
diff --git a/src/views/infra/demo03/DemoStudentAddressList.vue b/src/views/infra/demo03/DemoStudentAddressList.vue
deleted file mode 100644
index 2354db56..00000000
--- a/src/views/infra/demo03/DemoStudentAddressList.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="手机" align="center" prop="mobile" />
-    </el-table>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: props.studentId,
-        mobile: '88888'
-      }
-    ]
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>
diff --git a/src/views/infra/demo03/DemoStudentContactList.vue b/src/views/infra/demo03/DemoStudentContactList.vue
deleted file mode 100644
index 76d46116..00000000
--- a/src/views/infra/demo03/DemoStudentContactList.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="手机" align="center" prop="mobile" />
-    </el-table>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: props.studentId,
-        mobile: '15601691300'
-      }
-    ]
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>
diff --git a/src/views/infra/demo03/DemoStudentForm.vue b/src/views/infra/demo03/DemoStudentForm.vue
deleted file mode 100644
index fe0788c4..00000000
--- a/src/views/infra/demo03/DemoStudentForm.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="字段 1" prop="field1">
-        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-      </el-form-item>
-      <el-form-item label="字段 2" prop="field2">
-        <el-input v-model="formData.field2" placeholder="请输入字段 2" />
-      </el-form-item>
-      <el-form-item label="字段 3" prop="field3">
-        <el-input v-model="formData.field3" placeholder="请输入字段 3" />
-      </el-form-item>
-    </el-form>
-    <!-- 子表的表单 -->
-    <el-tabs v-model="subTabsName">
-      <el-tab-pane label="联系人信息" name="DemoStudentContact">
-        <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-      <el-tab-pane label="地址信息" name="DemoStudentAddress">
-        <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-    </el-tabs>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentContactForm from './DemoStudentContactForm.vue'
-import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined
-})
-const formRules = reactive({
-  field2: [required]
-})
-const formRef = ref() // 表单 Ref
-
-/** 子表的表单 */
-const demoStudentContactFormRef = ref()
-const demoStudentAddressFormRef = ref()
-const subTabsName = ref('DemoStudentContact')
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    // debugger
-    formLoading.value = true
-    try {
-      // formData.value = await DemoStudentApi.getDemoStudent(id)
-      formData.value = {
-        id: id,
-        field1: '1',
-        field2: '22',
-        field3: '333'
-      }
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 校验子表单
-  try {
-    await demoStudentContactFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'DemoStudentContact'
-    return
-  }
-  try {
-    await demoStudentAddressFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'DemoStudentAddress'
-    return
-  }
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
-    // 拼接子表的数据
-    data.demoStudentContacts = demoStudentContactFormRef.value.getData()
-    data.demoStudentAddress = demoStudentAddressFormRef.value.getData()
-    if (formType.value === 'create') {
-      await DemoStudentApi.createDemoStudent(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DemoStudentApi.updateDemoStudent(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>

From 6c405ba42039fd849a387e53df0799419d7a6000 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Thu, 16 Nov 2023 23:48:20 +0800
Subject: [PATCH 14/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=B8=BB=E5=AD=90=E8=A1=A8=20inner?=
 =?UTF-8?q?=20=E6=A8=A1=E5=BC=8F=E7=9A=84=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo/demo03/inner/index.ts      |  53 ++++
 src/api/infra/demo11/index.ts                 |  57 ----
 src/api/infra/demo12/index.ts                 |  95 ------
 .../demo03/inner/Demo03StudentForm.vue}       | 116 ++++----
 .../inner/components/Demo03CourseForm.vue     | 100 +++++++
 .../inner/components/Demo03CourseList.vue     |  51 ++++
 .../inner/components/Demo03GradeForm.vue      |  72 +++++
 .../inner/components/Demo03GradeList.vue      |  55 ++++
 .../{demo11 => demo/demo03/inner}/index.vue   |  90 +++---
 .../infra/demo04/DemoStudentAddressForm.vue   |  58 ----
 .../infra/demo04/DemoStudentAddressList.vue   |  39 ---
 .../infra/demo04/DemoStudentContactForm.vue   |  96 ------
 .../infra/demo04/DemoStudentContactList.vue   |  70 -----
 src/views/infra/demo04/DemoStudentForm.vue    | 132 ---------
 src/views/infra/demo04/index.vue              | 184 ------------
 src/views/infra/demo11/Demo11StudentForm.vue  | 183 ------------
 .../components/Demo11StudentContactForm.vue   | 174 -----------
 .../components/Demo11StudentTeacherForm.vue   | 122 --------
 src/views/infra/demo12/Demo12StudentForm.vue  | 183 ------------
 .../components/Demo12StudentContactList.vue   | 148 ----------
 .../components/Demo12StudentTeacherForm.vue   | 155 ----------
 .../components/Demo12StudentTeacherList.vue   | 148 ----------
 src/views/infra/demo12/index.vue              | 278 ------------------
 src/views/infra/testDemo/index.vue            |   4 -
 24 files changed, 423 insertions(+), 2240 deletions(-)
 create mode 100644 src/api/infra/demo/demo03/inner/index.ts
 delete mode 100644 src/api/infra/demo11/index.ts
 delete mode 100644 src/api/infra/demo12/index.ts
 rename src/views/infra/{demo12/components/Demo12StudentContactForm.vue => demo/demo03/inner/Demo03StudentForm.vue} (61%)
 create mode 100644 src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue
 create mode 100644 src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue
 create mode 100644 src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue
 create mode 100644 src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue
 rename src/views/infra/{demo11 => demo/demo03/inner}/index.vue (74%)
 delete mode 100644 src/views/infra/demo04/DemoStudentAddressForm.vue
 delete mode 100644 src/views/infra/demo04/DemoStudentAddressList.vue
 delete mode 100644 src/views/infra/demo04/DemoStudentContactForm.vue
 delete mode 100644 src/views/infra/demo04/DemoStudentContactList.vue
 delete mode 100644 src/views/infra/demo04/DemoStudentForm.vue
 delete mode 100644 src/views/infra/demo04/index.vue
 delete mode 100644 src/views/infra/demo11/Demo11StudentForm.vue
 delete mode 100644 src/views/infra/demo11/components/Demo11StudentContactForm.vue
 delete mode 100644 src/views/infra/demo11/components/Demo11StudentTeacherForm.vue
 delete mode 100644 src/views/infra/demo12/Demo12StudentForm.vue
 delete mode 100644 src/views/infra/demo12/components/Demo12StudentContactList.vue
 delete mode 100644 src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
 delete mode 100644 src/views/infra/demo12/components/Demo12StudentTeacherList.vue
 delete mode 100644 src/views/infra/demo12/index.vue
 delete mode 100644 src/views/infra/testDemo/index.vue

diff --git a/src/api/infra/demo/demo03/inner/index.ts b/src/api/infra/demo/demo03/inner/index.ts
new file mode 100644
index 00000000..f15ee1dc
--- /dev/null
+++ b/src/api/infra/demo/demo03/inner/index.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+export interface Demo03StudentVO {
+  id: number
+  name: string
+  sex: number
+  birthday: Date
+  description: string
+}
+
+// 查询学生分页
+export const getDemo03StudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo03-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemo03Student = async (id: number) => {
+  return await request.get({ url: `/infra/demo03-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemo03Student = async (data: Demo03StudentVO) => {
+  return await request.post({ url: `/infra/demo03-student/create`, data })
+}
+
+// 修改学生
+export const updateDemo03Student = async (data: Demo03StudentVO) => {
+  return await request.put({ url: `/infra/demo03-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemo03Student = async (id: number) => {
+  return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemo03Student = async (params) => {
+  return await request.download({ url: `/infra/demo03-student/export-excel`, params })
+}
+
+// ==================== 子表(学生课程) ====================
+
+// 获得学生课程列表
+export const getDemo03CourseListByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-course/list-by-student-id?studentId=` + studentId })
+}
+
+// ==================== 子表(学生班级) ====================
+
+// 获得学生班级
+export const getDemo03GradeByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-grade/get-by-student-id?studentId=` + studentId })
+}
\ No newline at end of file
diff --git a/src/api/infra/demo11/index.ts b/src/api/infra/demo11/index.ts
deleted file mode 100644
index 4c4c7b4e..00000000
--- a/src/api/infra/demo11/index.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import request from '@/config/axios'
-
-export interface Demo11StudentVO {
-  id: number
-  name: string
-  description: string
-  birthday: Date
-  sex: number
-  enabled: boolean
-  avatar: string
-  video: string
-  memo: string
-}
-
-// 查询学生列表
-export const getDemo11StudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo11-student/page`, params })
-}
-
-// 查询学生详情
-export const getDemo11Student = async (id: number) => {
-  return await request.get({ url: `/infra/demo11-student/get?id=` + id })
-}
-
-// 新增学生
-export const createDemo11Student = async (data: Demo11StudentVO) => {
-  return await request.post({ url: `/infra/demo11-student/create`, data })
-}
-
-// 修改学生
-export const updateDemo11Student = async (data: Demo11StudentVO) => {
-  return await request.put({ url: `/infra/demo11-student/update`, data })
-}
-
-// 删除学生
-export const deleteDemo11Student = async (id: number) => {
-  return await request.delete({ url: `/infra/demo11-student/delete?id=` + id })
-}
-
-// 导出学生 Excel
-export const exportDemo11Student = async (params) => {
-  return await request.download({ url: `/infra/demo11-student/export-excel`, params })
-}
-
-// ==================== 子表(学生联系人) ====================
-
-// 获得学生联系人列表
-export const getDemo11StudentContactListByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo11-student/demo11-student/list-by-student-id?studentId=` + studentId })
-}
-
-// ==================== 子表(学生班主任) ====================
-
-// 获得学生班主任
-export const getDemo11StudentTeacherByStudentId = async (studentId) => {
-  return await request.get({ url: `/infra/demo11-student/demo11-student/get-by-student-id?studentId=` + studentId })
-}
\ No newline at end of file
diff --git a/src/api/infra/demo12/index.ts b/src/api/infra/demo12/index.ts
deleted file mode 100644
index 3f2be47e..00000000
--- a/src/api/infra/demo12/index.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import request from '@/config/axios'
-
-export interface Demo12StudentVO {
-  id: number
-  name: string
-  description: string
-  birthday: Date
-  sex: number
-  enabled: boolean
-  avatar: string
-  video: string
-  memo: string
-}
-
-// 查询学生列表
-export const getDemo12StudentPage = async (params) => {
-  return await request.get({ url: `/infra/demo12-student/page`, params })
-}
-
-// 查询学生详情
-export const getDemo12Student = async (id: number) => {
-  return await request.get({ url: `/infra/demo12-student/get?id=` + id })
-}
-
-// 新增学生
-export const createDemo12Student = async (data: Demo12StudentVO) => {
-  return await request.post({ url: `/infra/demo12-student/create`, data })
-}
-
-// 修改学生
-export const updateDemo12Student = async (data: Demo12StudentVO) => {
-  return await request.put({ url: `/infra/demo12-student/update`, data })
-}
-
-// 删除学生
-export const deleteDemo12Student = async (id: number) => {
-  return await request.delete({ url: `/infra/demo12-student/delete?id=` + id })
-}
-
-// 导出学生 Excel
-export const exportDemo12Student = async (params) => {
-  return await request.download({ url: `/infra/demo12-student/export-excel`, params })
-}
-
-// ==================== 子表(学生联系人) ====================
-
-// 获得学生联系人分页
-export const getDemo12StudentContactPage = async (params) => {
-  return await request.get({ url: `/infra/demo12-student/demo12-student-contact/page`, params })
-}
-// 新增学生联系人
-export const createDemo12StudentContact = async (data) => {
-  return await request.post({ url: `/infra/demo12-student/demo12-student-contact/create`, data })
-}
-
-// 修改学生联系人
-export const updateDemo12StudentContact = async (data) => {
-  return await request.put({ url: `/infra/demo12-student/demo12-student-contact/update`, data })
-}
-
-// 删除学生联系人
-export const deleteDemo12StudentContact = async (id: number) => {
-  return await request.delete({ url: `/infra/demo12-student/demo12-student-contact/delete?id=` + id })
-}
-
-// 获得学生联系人
-export const getDemo12StudentContact = async (id: number) => {
-  return await request.get({ url: `/infra/demo12-student/demo12-student-contact/get?id=` + id })
-}
-
-// ==================== 子表(学生班主任) ====================
-
-// 获得学生班主任分页
-export const getDemo12StudentTeacherPage = async (params) => {
-  return await request.get({ url: `/infra/demo12-student/demo12-student-teacher/page`, params })
-}
-// 新增学生班主任
-export const createDemo12StudentTeacher = async (data) => {
-  return await request.post({ url: `/infra/demo12-student/demo12-student-teacher/create`, data })
-}
-
-// 修改学生班主任
-export const updateDemo12StudentTeacher = async (data) => {
-  return await request.put({ url: `/infra/demo12-student/demo12-student-teacher/update`, data })
-}
-
-// 删除学生班主任
-export const deleteDemo12StudentTeacher = async (id: number) => {
-  return await request.delete({ url: `/infra/demo12-student/demo12-student-teacher/delete?id=` + id })
-}
-
-// 获得学生班主任
-export const getDemo12StudentTeacher = async (id: number) => {
-  return await request.get({ url: `/infra/demo12-student/demo12-student-teacher/get?id=` + id })
-}
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentContactForm.vue b/src/views/infra/demo/demo03/inner/Demo03StudentForm.vue
similarity index 61%
rename from src/views/infra/demo12/components/Demo12StudentContactForm.vue
rename to src/views/infra/demo/demo03/inner/Demo03StudentForm.vue
index e246678b..fe9327b9 100644
--- a/src/views/infra/demo12/components/Demo12StudentContactForm.vue
+++ b/src/views/infra/demo/demo03/inner/Demo03StudentForm.vue
@@ -10,8 +10,16 @@
       <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 label="性别" prop="sex">
+        <el-radio-group v-model="formData.sex">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
       </el-form-item>
       <el-form-item label="出生日期" prop="birthday">
         <el-date-picker
@@ -21,37 +29,19 @@
           placeholder="选择出生日期"
         />
       </el-form-item>
-      <el-form-item label="性别" prop="sex">
-        <el-select v-model="formData.sex" placeholder="请选择性别">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-radio-group v-model="formData.enabled">
-          <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="头像" prop="avatar">
-        <UploadImg v-model="formData.avatar" />
-      </el-form-item>
-      <el-form-item label="附件" prop="video">
-        <UploadFile v-model="formData.video" />
-      </el-form-item>
-      <el-form-item label="备注" prop="memo">
-        <Editor v-model="formData.memo" height="150px" />
+      <el-form-item label="简介" prop="description">
+        <Editor v-model="formData.description" height="150px" />
       </el-form-item>
     </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="学生课程" name="demo03Course">
+        <Demo03CourseForm ref="demo03CourseFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班级" name="demo03Grade">
+        <Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -59,8 +49,10 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo12StudentApi from '@/api/infra/demo12'
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/inner'
+import Demo03CourseForm from './components/Demo03CourseForm.vue'
+import Demo03GradeForm from './components/Demo03GradeForm.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -71,40 +63,35 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
-  studentId: undefined,
   name: undefined,
-  description: undefined,
-  birthday: undefined,
   sex: undefined,
-  enabled: undefined,
-  avatar: undefined,
-  video: undefined,
-  memo: undefined
+  birthday: undefined,
+  description: undefined
 })
 const formRules = reactive({
-  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
   birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
+/** 子表的表单 */
+const subTabsName = ref('demo03Course')
+const demo03CourseFormRef = ref()
+const demo03GradeFormRef = ref()
+
 /** 打开弹窗 */
-const open = async (type: string, id?: number, studentId: number) => {
+const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
-  formData.value.studentId = studentId
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await Demo12StudentApi.getDemo12StudentContact(id)
+      formData.value = await Demo03StudentApi.getDemo03Student(id)
     } finally {
       formLoading.value = false
     }
@@ -117,15 +104,31 @@ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成
 const submitForm = async () => {
   // 校验表单
   await formRef.value.validate()
+  // 校验子表单
+  try {
+    await demo03CourseFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo03Course'
+    return
+  }
+  try {
+    await demo03GradeFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'demo03Grade'
+    return
+  }
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value
+    const data = formData.value as unknown as Demo03StudentApi.Demo03StudentVO
+    // 拼接子表的数据
+    data.demo03Courses = demo03CourseFormRef.value.getData()
+    data.demo03Grade = demo03GradeFormRef.value.getData()
     if (formType.value === 'create') {
-      await Demo12StudentApi.createDemo12StudentContact(data)
+      await Demo03StudentApi.createDemo03Student(data)
       message.success(t('common.createSuccess'))
     } else {
-      await Demo12StudentApi.updateDemo12StudentContact(data)
+      await Demo03StudentApi.updateDemo03Student(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -140,15 +143,10 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: undefined,
-    studentId: undefined,
     name: undefined,
-    description: undefined,
-    birthday: undefined,
     sex: undefined,
-    enabled: undefined,
-    avatar: undefined,
-    video: undefined,
-    memo: undefined
+    birthday: undefined,
+    description: undefined
   }
   formRef.value?.resetFields()
 }
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue b/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue
new file mode 100644
index 00000000..87057513
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue
@@ -0,0 +1,100 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+       <el-table-column label="名字" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+            <el-input v-model="row.name" placeholder="请输入名字" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="分数" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.score`" :rules="formRules.score" class="mb-0px!">
+            <el-input v-model="row.score" placeholder="请输入分数" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加学生课程</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/inner'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  score: [{ required: true, message: '分数不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = []
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      formData.value = await Demo03StudentApi.getDemo03CourseListByStudentId(val)
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    score: undefined
+  }
+  row.studentId = props.studentId
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue b/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue
new file mode 100644
index 00000000..d912fc5d
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue
@@ -0,0 +1,51 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="分数" align="center" prop="score" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/inner'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await Demo03StudentApi.getDemo03CourseListByStudentId(props.studentId)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue b/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue
new file mode 100644
index 00000000..e0eeb192
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue
@@ -0,0 +1,72 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+     <el-form-item label="名字" prop="name">
+      <el-input v-model="formData.name" placeholder="请输入名字" />
+    </el-form-item>
+    <el-form-item label="班主任" prop="teacher">
+      <el-input v-model="formData.teacher" placeholder="请输入班主任" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/inner'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = {
+      id: undefined,
+      studentId: undefined,
+      name: undefined,
+      teacher: undefined,
+    }
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      const data = await Demo03StudentApi.getDemo03GradeByStudentId(val)
+      if (!data) {
+        return
+      }
+      formData.value = data
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue b/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue
new file mode 100644
index 00000000..96905414
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue
@@ -0,0 +1,55 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="班主任" align="center" prop="teacher" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/inner'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo03StudentApi.getDemo03GradeByStudentId(props.studentId)
+    if (!data) {
+      return
+    }
+    list.value.push(data)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo11/index.vue b/src/views/infra/demo/demo03/inner/index.vue
similarity index 74%
rename from src/views/infra/demo11/index.vue
rename to src/views/infra/demo/demo03/inner/index.vue
index 36529eb9..4ce6037d 100644
--- a/src/views/infra/demo11/index.vue
+++ b/src/views/infra/demo/demo03/inner/index.vue
@@ -17,16 +17,6 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="出生日期" prop="birthday">
-        <el-date-picker
-          v-model="queryParams.birthday"
-          value-format="YYYY-MM-DD"
-          type="date"
-          placeholder="选择出生日期"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
       <el-form-item label="性别" prop="sex">
         <el-select
           v-model="queryParams.sex"
@@ -42,21 +32,6 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-select
-          v-model="queryParams.enabled"
-          placeholder="请选择是否有效"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -75,7 +50,7 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['infra:demo11-student:create']"
+          v-hasPermi="['infra:demo03-student:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -84,7 +59,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['infra:demo11-student:export']"
+          v-hasPermi="['infra:demo03-student:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
@@ -95,9 +70,26 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <!-- 子表的列表 -->
+      <el-table-column type="expand">
+        <template #default="scope">
+          <el-tabs model-value="demo03Course">
+            <el-tab-pane label="学生课程" name="demo03Course">
+              <Demo03CourseList :student-id="scope.row.id" />
+            </el-tab-pane>
+            <el-tab-pane label="学生班级" name="demo03Grade">
+              <Demo03GradeList :student-id="scope.row.id" />
+            </el-tab-pane>
+          </el-tabs>
+        </template>
+      </el-table-column>
       <el-table-column label="编号" align="center" prop="id" />
       <el-table-column label="名字" align="center" prop="name" />
-      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
       <el-table-column
         label="出生日期"
         align="center"
@@ -105,19 +97,7 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="性别" align="center" prop="sex">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
-        </template>
-      </el-table-column>
-      <el-table-column label="是否有效" align="center" prop="enabled">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
-        </template>
-      </el-table-column>
-      <el-table-column label="头像" align="center" prop="avatar" />
-      <el-table-column label="附件" align="center" prop="video" />
-      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column label="简介" align="center" prop="description" />
       <el-table-column
         label="创建时间"
         align="center"
@@ -131,7 +111,7 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['infra:demo11-student:update']"
+            v-hasPermi="['infra:demo03-student:update']"
           >
             编辑
           </el-button>
@@ -139,7 +119,7 @@
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo11-student:delete']"
+            v-hasPermi="['infra:demo03-student:delete']"
           >
             删除
           </el-button>
@@ -156,32 +136,32 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <Demo11StudentForm ref="formRef" @success="getList" />
+  <Demo03StudentForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as Demo11StudentApi from '@/api/infra/demo11'
-import Demo11StudentForm from './Demo11StudentForm.vue'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/inner'
+import Demo03StudentForm from './Demo03StudentForm.vue'
+import Demo03CourseList from './components/Demo03CourseList.vue'
+import Demo03GradeList from './components/Demo03GradeList.vue'
 
-defineOptions({ name: 'InfraDemo11Student' })
+defineOptions({ name: 'Demo03Student' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: null,
-  birthday: null,
-  birthday: [],
   sex: null,
-  enabled: null,
+  description: null,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
@@ -191,7 +171,7 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    const data = await Demo11StudentApi.getDemo11StudentPage(queryParams)
+    const data = await Demo03StudentApi.getDemo03StudentPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -223,7 +203,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await Demo11StudentApi.deleteDemo11Student(id)
+    await Demo03StudentApi.deleteDemo03Student(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -237,7 +217,7 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await Demo11StudentApi.exportDemo11Student(queryParams)
+    const data = await Demo03StudentApi.exportDemo03Student(queryParams)
     download.excel(data, '学生.xls')
   } catch {
   } finally {
diff --git a/src/views/infra/demo04/DemoStudentAddressForm.vue b/src/views/infra/demo04/DemoStudentAddressForm.vue
deleted file mode 100644
index af4cd8f1..00000000
--- a/src/views/infra/demo04/DemoStudentAddressForm.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    label-width="100px"
-    v-loading="formLoading"
-  >
-    <el-form-item label="子字段 1" prop="field1">
-      <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-    </el-form-item>
-    <el-form-item label="子字段 2" prop="field2">
-      <el-input v-model="formData.field2" placeholder="请输入字段 2" />
-    </el-form-item>
-    <el-form-item label="子字段 3" prop="field3">
-      <el-input v-model="formData.field3" placeholder="请输入字段 3" />
-    </el-form-item>
-  </el-form>
-</template>
-<script setup lang="ts">
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({})
-const formRules = reactive({
-  field1: [required]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  (val) => {
-    if (val) {
-      formData.value = {
-        field2: '番茄',
-        field3: '西瓜'
-      }
-    } else {
-      formData.value = {}
-    }
-  },
-  { immediate: true }
-)
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 **/
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>
diff --git a/src/views/infra/demo04/DemoStudentAddressList.vue b/src/views/infra/demo04/DemoStudentAddressList.vue
deleted file mode 100644
index 92143456..00000000
--- a/src/views/infra/demo04/DemoStudentAddressList.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="手机" align="center" prop="mobile" />
-    </el-table>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-
-// TODO 芋艿:暂时没改
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: props.studentId,
-        mobile: '88888'
-      }
-    ]
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>
diff --git a/src/views/infra/demo04/DemoStudentContactForm.vue b/src/views/infra/demo04/DemoStudentContactForm.vue
deleted file mode 100644
index c8744eb0..00000000
--- a/src/views/infra/demo04/DemoStudentContactForm.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="字段 1" prop="field1">
-        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentContactForm from './DemoStudentContactForm.vue'
-import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined
-})
-const formRules = reactive({
-  field2: [required]
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    // debugger
-    formLoading.value = true
-    try {
-      // formData.value = await DemoStudentApi.getDemoStudent(id)
-      formData.value = {
-        id: id,
-        field1: '1',
-        field2: '22',
-        field3: '333'
-      }
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
-    if (formType.value === 'create') {
-      // await DemoStudentApi.createDemoStudent(data) // TODO 芋艿:临时去掉
-      message.success(t('common.createSuccess'))
-    } else {
-      await DemoStudentApi.updateDemoStudent(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>
diff --git a/src/views/infra/demo04/DemoStudentContactList.vue b/src/views/infra/demo04/DemoStudentContactList.vue
deleted file mode 100644
index b944c407..00000000
--- a/src/views/infra/demo04/DemoStudentContactList.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-button type="primary" plain @click="openForm('create')">
-      <Icon icon="ep:plus" class="mr-5px" /> 新增
-    </el-button>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="手机" align="center" prop="mobile" />
-    </el-table>
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <DemoStudentContactForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import DemoStudentContactForm from './DemoStudentContactForm.vue'
-
-const props = defineProps<{
-  studentId: undefined // 学生编号
-}>()
-const loading = ref(false) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  studentId: undefined
-})
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  (val) => {
-    queryParams.studentId = val
-    handleQuery()
-  },
-  { immediate: false }
-)
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: props.studentId,
-        mobile: '15601691300'
-      }
-    ]
-    total.value = 10
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-</script>
diff --git a/src/views/infra/demo04/DemoStudentForm.vue b/src/views/infra/demo04/DemoStudentForm.vue
deleted file mode 100644
index fe0788c4..00000000
--- a/src/views/infra/demo04/DemoStudentForm.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="字段 1" prop="field1">
-        <el-input v-model="formData.field1" placeholder="请输入字段 1" />
-      </el-form-item>
-      <el-form-item label="字段 2" prop="field2">
-        <el-input v-model="formData.field2" placeholder="请输入字段 2" />
-      </el-form-item>
-      <el-form-item label="字段 3" prop="field3">
-        <el-input v-model="formData.field3" placeholder="请输入字段 3" />
-      </el-form-item>
-    </el-form>
-    <!-- 子表的表单 -->
-    <el-tabs v-model="subTabsName">
-      <el-tab-pane label="联系人信息" name="DemoStudentContact">
-        <DemoStudentContactForm ref="demoStudentContactFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-      <el-tab-pane label="地址信息" name="DemoStudentAddress">
-        <DemoStudentAddressForm ref="demoStudentAddressFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-    </el-tabs>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentContactForm from './DemoStudentContactForm.vue'
-import DemoStudentAddressForm from './DemoStudentAddressForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined
-})
-const formRules = reactive({
-  field2: [required]
-})
-const formRef = ref() // 表单 Ref
-
-/** 子表的表单 */
-const demoStudentContactFormRef = ref()
-const demoStudentAddressFormRef = ref()
-const subTabsName = ref('DemoStudentContact')
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    // debugger
-    formLoading.value = true
-    try {
-      // formData.value = await DemoStudentApi.getDemoStudent(id)
-      formData.value = {
-        id: id,
-        field1: '1',
-        field2: '22',
-        field3: '333'
-      }
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 校验子表单
-  try {
-    await demoStudentContactFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'DemoStudentContact'
-    return
-  }
-  try {
-    await demoStudentAddressFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'DemoStudentAddress'
-    return
-  }
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as DemoStudentApi.DemoStudentVO
-    // 拼接子表的数据
-    data.demoStudentContacts = demoStudentContactFormRef.value.getData()
-    data.demoStudentAddress = demoStudentAddressFormRef.value.getData()
-    if (formType.value === 'create') {
-      await DemoStudentApi.createDemoStudent(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await DemoStudentApi.updateDemoStudent(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>
diff --git a/src/views/infra/demo04/index.vue b/src/views/infra/demo04/index.vue
deleted file mode 100644
index 9a6b8bfe..00000000
--- a/src/views/infra/demo04/index.vue
+++ /dev/null
@@ -1,184 +0,0 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button type="primary" plain @click="openForm('create')">
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['infra:demo-student:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="true"
-      highlight-current-row
-      @current-change="handleCurrentChange"
-    >
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button link type="primary" @click="openForm('update', scope.row.id)">
-            编辑
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo-student:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 子列表 -->
-  <ContentWrap>
-    <el-tabs model-value="DemoStudentContact">
-      <el-tab-pane label="联系人信息" name="DemoStudentContact">
-        <DemoStudentContactList :student-id="currentRow.id" />
-      </el-tab-pane>
-      <el-tab-pane label="地址信息" name="DemoStudentAddress">
-        <DemoStudentAddressList :student-id="currentRow.id" />
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <DemoStudentForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import download from '@/utils/download'
-import * as DemoStudentApi from '@/api/infra/demo02'
-import DemoStudentForm from './DemoStudentForm.vue'
-import DemoStudentContactList from './DemoStudentContactList.vue'
-import DemoStudentAddressList from './DemoStudentAddressList.vue'
-
-defineOptions({ name: 'InfraDemoStudent' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-const currentRow = ref({}) // 选中行
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // const data = await DemoStudentApi.getDemoStudentPage(queryParams)
-    list.value = [
-      {
-        id: 1
-      },
-      {
-        id: 10
-      }
-    ]
-    total.value = 10
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-// const demoStudentContactFormRef = ref()
-const openForm = (type: string, id?: number) => {
-  // console.log(demoStudentContactFormRef, 'xx demoStudentContactFormRef xx')
-  // demoStudentContactFormRef.value.validate()
-  formRef.value.open(type, id)
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await DemoStudentApi.deleteDemoStudent(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await DemoStudentApi.exportDemoStudent(queryParams)
-    download.excel(data, '学生.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
-/** 选中行操作 */
-const handleCurrentChange = (row) => {
-  console.log(currentRow.value, '==== currentRow.value ====')
-  console.log(row, '==== row ====')
-  currentRow.value = row
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>
diff --git a/src/views/infra/demo11/Demo11StudentForm.vue b/src/views/infra/demo11/Demo11StudentForm.vue
deleted file mode 100644
index 39c21a2b..00000000
--- a/src/views/infra/demo11/Demo11StudentForm.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <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
-          v-model="formData.birthday"
-          type="date"
-          value-format="x"
-          placeholder="选择出生日期"
-        />
-      </el-form-item>
-      <el-form-item label="性别" prop="sex">
-        <el-select v-model="formData.sex" placeholder="请选择性别">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-radio-group v-model="formData.enabled">
-          <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="头像">
-        <UploadImg v-model="formData.avatar" />
-      </el-form-item>
-      <el-form-item label="附件" prop="video">
-        <UploadFile v-model="formData.video" />
-      </el-form-item>
-      <el-form-item label="备注" prop="memo">
-        <Editor v-model="formData.memo" height="150px" />
-      </el-form-item>
-    </el-form>
-    <!-- 子表的表单 -->
-    <el-tabs v-model="subTabsName">
-      <el-tab-pane label="学生联系人" name="demo11StudentContact">
-        <Demo11StudentContactForm ref="demo11StudentContactFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-      <el-tab-pane label="学生班主任" name="demo11StudentTeacher">
-        <Demo11StudentTeacherForm ref="demo11StudentTeacherFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-    </el-tabs>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo11StudentApi from '@/api/infra/demo11'
-import Demo11StudentContactForm from './components/Demo11StudentContactForm.vue'
-import Demo11StudentTeacherForm from './components/Demo11StudentTeacherForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  name: undefined,
-  description: undefined,
-  birthday: undefined,
-  sex: undefined,
-  enabled: undefined,
-  avatar: undefined,
-  video: undefined,
-  memo: undefined
-})
-const formRules = reactive({
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 子表的表单 */
-const subTabsName = ref('demo11StudentContact')
-const demo11StudentContactFormRef = ref()
-const demo11StudentTeacherFormRef = ref()
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await Demo11StudentApi.getDemo11Student(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 校验子表单
-  try {
-    await demo11StudentContactFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'demo11StudentContact'
-    return
-  }
-  try {
-    await demo11StudentTeacherFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'demo11StudentTeacher'
-    return
-  }
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as Demo11StudentApi.Demo11StudentVO
-    // 拼接子表的数据
-    data.demo11StudentContacts = demo11StudentContactFormRef.value.getData()
-    data.demo11StudentTeacher = demo11StudentTeacherFormRef.value.getData()
-    if (formType.value === 'create') {
-      await Demo11StudentApi.createDemo11Student(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await Demo11StudentApi.updateDemo11Student(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: undefined,
-    description: undefined,
-    birthday: undefined,
-    sex: undefined,
-    enabled: undefined,
-    avatar: undefined,
-    video: undefined,
-    memo: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo11/components/Demo11StudentContactForm.vue b/src/views/infra/demo11/components/Demo11StudentContactForm.vue
deleted file mode 100644
index ba900017..00000000
--- a/src/views/infra/demo11/components/Demo11StudentContactForm.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    v-loading="formLoading"
-    label-width="0px"
-    :inline-message="true"
-  >
-    <el-table :data="formData" class="-mt-10px">
-      <el-table-column label="序号" type="index" width="100" />
-      <el-table-column label="名字" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
-            <el-input v-model="row.name" placeholder="请输入名字" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="简介" width="200">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
-            <el-input v-model="row.description" type="textarea" placeholder="请输入简介" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="出生日期" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
-            <el-date-picker
-              v-model="row.birthday"
-              type="date"
-              value-format="x"
-              placeholder="选择出生日期"
-            />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="性别" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
-            <el-select v-model="row.sex" placeholder="请选择性别">
-                <el-option
-                  v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
-            </el-select>
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="是否有效" width="150">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.enabled`" :rules="formRules.enabled" class="mb-0px!">
-            <el-radio-group v-model="row.enabled">
-                <el-radio
-                  v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                  :key="dict.value"
-                  :label="dict.value"
-                >
-                  {{ dict.label }}
-                </el-radio>
-            </el-radio-group>
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="头像" width="200">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
-            <UploadImg v-model="row.avatar" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="附件" width="200">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
-            <UploadFile v-model="row.video" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column label="备注" width="400">
-        <template #default="{ row, $index }">
-          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
-            <Editor v-model="row.memo" height="150px" />
-          </el-form-item>
-        </template>
-      </el-table-column>
-      <el-table-column align="center" fixed="right" label="操作" width="60">
-        <template #default="{ $index }">
-          <el-button @click="handleDelete($index)" link>—</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </el-form>
-  <el-row justify="center" class="mt-3">
-    <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
-  </el-row>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo11StudentApi from '@/api/infra/demo11'
-
-const props = defineProps<{
-  studentId: undefined // 学生编号(主表的关联字段)
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref([])
-const formRules = reactive({
-  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  async (val) => {
-    // 情况一:val 为空,说明是新增,则置空
-    if (!val) {
-      formData.value = []
-      return;
-    }
-    // 情况二:val 非空,说明是修改,则加载数据
-    try {
-      formLoading.value = true
-      formData.value = await Demo11StudentApi.getDemo11StudentContactListByStudentId(val)
-    } finally {
-      formLoading.value = false
-    }
-  },
-  { immediate: true }
-)
-
-/** 新增按钮操作 */
-const handleAdd = () => {
-  const row = {
-    id: undefined,
-    studentId: undefined,
-    name: undefined,
-    description: undefined,
-    birthday: undefined,
-    sex: undefined,
-    enabled: undefined,
-    avatar: undefined,
-    video: undefined,
-    memo: undefined
-  }
-  row.studentId = props.studentId
-  formData.value.push(row)
-}
-
-/** 删除按钮操作 */
-const handleDelete = (index) => {
-  formData.value.splice(index, 1)
-}
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 */
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo11/components/Demo11StudentTeacherForm.vue b/src/views/infra/demo11/components/Demo11StudentTeacherForm.vue
deleted file mode 100644
index a8697be1..00000000
--- a/src/views/infra/demo11/components/Demo11StudentTeacherForm.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-  <el-form
-    ref="formRef"
-    :model="formData"
-    :rules="formRules"
-    label-width="100px"
-    v-loading="formLoading"
-  >
-    <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
-        v-model="formData.birthday"
-        type="date"
-        value-format="x"
-        placeholder="选择出生日期"
-      />
-    </el-form-item>
-    <el-form-item label="性别" prop="sex">
-      <el-select v-model="formData.sex" placeholder="请选择性别">
-        <el-option
-          v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-          :key="dict.value"
-          :label="dict.label"
-          :value="dict.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="是否有效" prop="enabled">
-      <el-radio-group v-model="formData.enabled">
-        <el-radio
-          v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-          :key="dict.value"
-          :label="dict.value"
-          >
-          {{ dict.label }}
-        </el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item label="头像" prop="avatar">
-      <UploadImg v-model="formData.avatar" />
-    </el-form-item>
-    <el-form-item label="附件" prop="video">
-      <UploadFile v-model="formData.video" />
-    </el-form-item>
-    <el-form-item label="备注" prop="memo">
-      <Editor v-model="formData.memo" height="150px" />
-    </el-form-item>
-  </el-form>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo11StudentApi from '@/api/infra/demo11'
-
-const props = defineProps<{
-  studentId: undefined // 学生编号(主表的关联字段)
-}>()
-const formLoading = ref(false) // 表单的加载中
-const formData = ref([])
-const formRules = reactive({
-  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  async (val) => {
-    // 1. 重置表单
-    formData.value = {
-      id: undefined,
-      studentId: undefined,
-      name: undefined,
-      description: undefined,
-      birthday: undefined,
-      sex: undefined,
-      enabled: undefined,
-      avatar: undefined,
-      video: undefined,
-      memo: undefined,
-    }
-    // 2. val 非空,则加载数据
-    if (!val) {
-      return;
-    }
-    try {
-      formLoading.value = true
-      const data = await Demo11StudentApi.getDemo11StudentTeacherByStudentId(val)
-      if (!data) {
-        return
-      }
-      formData.value = data
-    } finally {
-      formLoading.value = false
-    }
-  },
-  { immediate: true }
-)
-
-/** 表单校验 */
-const validate = () => {
-  return formRef.value.validate()
-}
-
-/** 表单值 */
-const getData = () => {
-  return formData.value
-}
-
-defineExpose({ validate, getData })
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/Demo12StudentForm.vue b/src/views/infra/demo12/Demo12StudentForm.vue
deleted file mode 100644
index af4e2733..00000000
--- a/src/views/infra/demo12/Demo12StudentForm.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <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
-          v-model="formData.birthday"
-          type="date"
-          value-format="x"
-          placeholder="选择出生日期"
-        />
-      </el-form-item>
-      <el-form-item label="性别" prop="sex">
-        <el-select v-model="formData.sex" placeholder="请选择性别">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-radio-group v-model="formData.enabled">
-          <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="头像" prop="avatar">
-        <UploadImg v-model="formData.avatar" />
-      </el-form-item>
-      <el-form-item label="附件" prop="video">
-        <UploadFile v-model="formData.video" />
-      </el-form-item>
-      <el-form-item label="备注" prop="memo">
-        <Editor v-model="formData.memo" height="150px" />
-      </el-form-item>
-    </el-form>
-    <!-- 子表的表单 -->
-    <el-tabs v-model="subTabsName">
-      <el-tab-pane label="学生联系人" name="demo12StudentContact">
-        <Demo12StudentContactForm ref="demo12StudentContactFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-      <el-tab-pane label="学生班主任" name="demo12StudentTeacher">
-        <Demo12StudentTeacherForm ref="demo12StudentTeacherFormRef" :student-id="formData.id" />
-      </el-tab-pane>
-    </el-tabs>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo12StudentApi from '@/api/infra/demo12'
-import Demo12StudentContactForm from './components/Demo12StudentContactForm.vue'
-import Demo12StudentTeacherForm from './components/Demo12StudentTeacherForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  name: undefined,
-  description: undefined,
-  birthday: undefined,
-  sex: undefined,
-  enabled: undefined,
-  avatar: undefined,
-  video: undefined,
-  memo: undefined
-})
-const formRules = reactive({
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 子表的表单 */
-const subTabsName = ref('demo12StudentContact')
-const demo12StudentContactFormRef = ref()
-const demo12StudentTeacherFormRef = ref()
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await Demo12StudentApi.getDemo12Student(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 校验子表单
-  try {
-    await demo12StudentContactFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'demo12StudentContact'
-    return
-  }
-  try {
-    await demo12StudentTeacherFormRef.value.validate()
-  } catch (e) {
-    subTabsName.value = 'demo12StudentTeacher'
-    return
-  }
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as Demo12StudentApi.Demo12StudentVO
-    // 拼接子表的数据
-    data.demo12StudentContacts = demo12StudentContactFormRef.value.getData()
-    data.demo12StudentTeacher = demo12StudentTeacherFormRef.value.getData()
-    if (formType.value === 'create') {
-      await Demo12StudentApi.createDemo12Student(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await Demo12StudentApi.updateDemo12Student(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: undefined,
-    description: undefined,
-    birthday: undefined,
-    sex: undefined,
-    enabled: undefined,
-    avatar: undefined,
-    video: undefined,
-    memo: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentContactList.vue b/src/views/infra/demo12/components/Demo12StudentContactList.vue
deleted file mode 100644
index b9b1e717..00000000
--- a/src/views/infra/demo12/components/Demo12StudentContactList.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-button
-      type="primary"
-      plain
-      @click="openForm('create')"
-      v-hasPermi="['infra:demo12-student:create']"
-    >
-      <Icon icon="ep:plus" class="mr-5px" /> 新增
-    </el-button>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="学生编号" align="center" prop="studentId" />
-      <el-table-column label="名字" align="center" prop="name" />
-      <el-table-column label="简介" align="center" prop="description" />
-      <el-table-column
-        label="出生日期"
-        align="center"
-        prop="birthday"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="性别" align="center" prop="sex">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
-        </template>
-      </el-table-column>
-      <el-table-column label="是否有效" align="center" prop="enabled">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
-        </template>
-      </el-table-column>
-      <el-table-column label="头像" align="center" prop="avatar" />
-      <el-table-column label="附件" align="center" prop="video" />
-      <el-table-column label="备注" align="center" prop="memo" />
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['infra:demo12-student:update']"
-          >
-            编辑
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo12-student:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-    <!-- 表单弹窗:添加/修改 -->
-    <Demo12StudentContactForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import { DICT_TYPE } from '@/utils/dict'
-import * as Demo12StudentApi from '@/api/infra/demo12'
-import Demo12StudentContactForm from './Demo12StudentContactForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const props = defineProps<{
-  studentId: undefined // 学生编号(主表的关联字段)
-}>()
-const loading = ref(false) // 列表的加载中
-const list = ref([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  studentId: undefined
-})
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  (val) => {
-    queryParams.studentId = val
-    handleQuery()
-  },
-  { immediate: false }
-)
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await Demo12StudentApi.getDemo12StudentContactPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  if (!props.studentId) {
-    message.error('请选择一个学生')
-    return
-  }
-  formRef.value.open(type, id, props.studentId)
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await Demo12StudentApi.deleteDemo12StudentContact(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue b/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
deleted file mode 100644
index 92e2b373..00000000
--- a/src/views/infra/demo12/components/Demo12StudentTeacherForm.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <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
-          v-model="formData.birthday"
-          type="date"
-          value-format="x"
-          placeholder="选择出生日期"
-        />
-      </el-form-item>
-      <el-form-item label="性别" prop="sex">
-        <el-select v-model="formData.sex" placeholder="请选择性别">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-radio-group v-model="formData.enabled">
-          <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="头像" prop="avatar">
-        <UploadImg v-model="formData.avatar" />
-      </el-form-item>
-      <el-form-item label="附件" prop="video">
-        <UploadFile v-model="formData.video" />
-      </el-form-item>
-      <el-form-item label="备注" prop="memo">
-        <Editor v-model="formData.memo" height="150px" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as Demo12StudentApi from '@/api/infra/demo12'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  studentId: undefined,
-  name: undefined,
-  description: undefined,
-  birthday: undefined,
-  sex: undefined,
-  enabled: undefined,
-  avatar: undefined,
-  video: undefined,
-  memo: undefined
-})
-const formRules = reactive({
-  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
-  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
-  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
-  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
-  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
-  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
-  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number, studentId: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  formData.value.studentId = studentId
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await Demo12StudentApi.getDemo12StudentTeacher(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value
-    if (formType.value === 'create') {
-      await Demo12StudentApi.createDemo12StudentTeacher(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await Demo12StudentApi.updateDemo12StudentTeacher(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    studentId: undefined,
-    name: undefined,
-    description: undefined,
-    birthday: undefined,
-    sex: undefined,
-    enabled: undefined,
-    avatar: undefined,
-    video: undefined,
-    memo: undefined
-  }
-  formRef.value?.resetFields()
-}
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/components/Demo12StudentTeacherList.vue b/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
deleted file mode 100644
index d0594534..00000000
--- a/src/views/infra/demo12/components/Demo12StudentTeacherList.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-button
-      type="primary"
-      plain
-      @click="openForm('create')"
-      v-hasPermi="['infra:demo12-student:create']"
-    >
-      <Icon icon="ep:plus" class="mr-5px" /> 新增
-    </el-button>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="学生编号" align="center" prop="studentId" />
-      <el-table-column label="名字" align="center" prop="name" />
-      <el-table-column label="简介" align="center" prop="description" />
-      <el-table-column
-        label="出生日期"
-        align="center"
-        prop="birthday"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="性别" align="center" prop="sex">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
-        </template>
-      </el-table-column>
-      <el-table-column label="是否有效" align="center" prop="enabled">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
-        </template>
-      </el-table-column>
-      <el-table-column label="头像" align="center" prop="avatar" />
-      <el-table-column label="附件" align="center" prop="video" />
-      <el-table-column label="备注" align="center" prop="memo" />
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['infra:demo12-student:update']"
-          >
-            编辑
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo12-student:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-    <!-- 表单弹窗:添加/修改 -->
-    <Demo12StudentTeacherForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import { DICT_TYPE } from '@/utils/dict'
-import * as Demo12StudentApi from '@/api/infra/demo12'
-import Demo12StudentTeacherForm from './Demo12StudentTeacherForm.vue'
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const props = defineProps<{
-  studentId: undefined // 学生编号(主表的关联字段)
-}>()
-const loading = ref(false) // 列表的加载中
-const list = ref([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  studentId: undefined
-})
-
-/** 监听主表的关联字段的变化,加载对应的子表数据 */
-watch(
-  () => props.studentId,
-  (val) => {
-    queryParams.studentId = val
-    handleQuery()
-  },
-  { immediate: false }
-)
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await Demo12StudentApi.getDemo12StudentTeacherPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  if (!props.studentId) {
-    message.error('请选择一个学生')
-    return
-  }
-  formRef.value.open(type, id, props.studentId)
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await Demo12StudentApi.deleteDemo12StudentTeacher(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-</script>
\ No newline at end of file
diff --git a/src/views/infra/demo12/index.vue b/src/views/infra/demo12/index.vue
deleted file mode 100644
index 47ec7f21..00000000
--- a/src/views/infra/demo12/index.vue
+++ /dev/null
@@ -1,278 +0,0 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="名字" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入名字"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="出生日期" prop="birthday">
-        <el-date-picker
-          v-model="queryParams.birthday"
-          value-format="YYYY-MM-DD"
-          type="date"
-          placeholder="选择出生日期"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="性别" prop="sex">
-        <el-select
-          v-model="queryParams.sex"
-          placeholder="请选择性别"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="是否有效" prop="enabled">
-        <el-select
-          v-model="queryParams.enabled"
-          placeholder="请选择是否有效"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['infra:demo12-student:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['infra:demo12-student:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="true"
-      highlight-current-row
-      @current-change="handleCurrentChange"
-    >
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="名字" align="center" prop="name" />
-      <el-table-column label="简介" align="center" prop="description" />
-      <el-table-column
-        label="出生日期"
-        align="center"
-        prop="birthday"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="性别" align="center" prop="sex">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
-        </template>
-      </el-table-column>
-      <el-table-column label="是否有效" align="center" prop="enabled">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
-        </template>
-      </el-table-column>
-      <el-table-column label="头像" align="center" prop="avatar" />
-      <el-table-column label="附件" align="center" prop="video" />
-      <el-table-column label="备注" align="center" prop="memo" />
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['infra:demo12-student:update']"
-          >
-            编辑
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['infra:demo12-student:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <Demo12StudentForm ref="formRef" @success="getList" />
-  <!-- 子表的列表 -->
-  <ContentWrap>
-    <el-tabs model-value="demo12StudentContact">
-      <el-tab-pane label="学生联系人" name="demo12StudentContact">
-        <Demo12StudentContactList :student-id="currentRow.id" />
-      </el-tab-pane>
-      <el-tab-pane label="学生班主任" name="demo12StudentTeacher">
-        <Demo12StudentTeacherList :student-id="currentRow.id" />
-      </el-tab-pane>
-    </el-tabs>
-  </ContentWrap>
-</template>
-
-<script setup lang="ts">
-import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
-import * as Demo12StudentApi from '@/api/infra/demo12'
-import Demo12StudentForm from './Demo12StudentForm.vue'
-import Demo12StudentContactList from './components/Demo12StudentContactList.vue'
-import Demo12StudentTeacherList from './components/Demo12StudentTeacherList.vue'
-
-defineOptions({ name: 'InfraDemo12Student' })
-
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  name: null,
-  birthday: null,
-  birthday: [],
-  sex: null,
-  enabled: null,
-  createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await Demo12StudentApi.getDemo12StudentPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
-
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await Demo12StudentApi.deleteDemo12Student(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await Demo12StudentApi.exportDemo12Student(queryParams)
-    download.excel(data, '学生.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
-/** 选中行操作 */
-const currentRow = ref({}) // 选中行
-const handleCurrentChange = (row) => {
-  currentRow.value = row
-}
-
-/** 初始化 **/
-onMounted(() => {
-  getList()
-})
-</script>
\ No newline at end of file
diff --git a/src/views/infra/testDemo/index.vue b/src/views/infra/testDemo/index.vue
deleted file mode 100644
index ca6a5b07..00000000
--- a/src/views/infra/testDemo/index.vue
+++ /dev/null
@@ -1,4 +0,0 @@
-<template>
-  <div>index</div>
-</template>
-<script lang="ts" setup></script>

From a42d0695fec0e2831c5b3b09593c93059ce67703 Mon Sep 17 00:00:00 2001
From: YunaiV <zhijiantianya@gmail.com>
Date: Fri, 17 Nov 2023 13:21:17 +0800
Subject: [PATCH 15/15] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?=
 =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=E4=B8=BB=E5=AD=90=E8=A1=A8=20erp=20?=
 =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=9A=84=E7=A4=BA=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/infra/demo/demo03/erp/index.ts        |  91 +++++++
 .../demo/demo03/erp/Demo03StudentForm.vue     | 121 +++++++++
 .../erp/components/Demo03CourseForm.vue       |  99 +++++++
 .../erp/components/Demo03CourseList.vue       | 126 +++++++++
 .../demo03/erp/components/Demo03GradeForm.vue |  99 +++++++
 .../demo03/erp/components/Demo03GradeList.vue | 126 +++++++++
 src/views/infra/demo/demo03/erp/index.vue     | 243 ++++++++++++++++++
 7 files changed, 905 insertions(+)
 create mode 100644 src/api/infra/demo/demo03/erp/index.ts
 create mode 100644 src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
 create mode 100644 src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
 create mode 100644 src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
 create mode 100644 src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
 create mode 100644 src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
 create mode 100644 src/views/infra/demo/demo03/erp/index.vue

diff --git a/src/api/infra/demo/demo03/erp/index.ts b/src/api/infra/demo/demo03/erp/index.ts
new file mode 100644
index 00000000..d408b630
--- /dev/null
+++ b/src/api/infra/demo/demo03/erp/index.ts
@@ -0,0 +1,91 @@
+import request from '@/config/axios'
+
+export interface Demo03StudentVO {
+  id: number
+  name: string
+  sex: number
+  birthday: Date
+  description: string
+}
+
+// 查询学生分页
+export const getDemo03StudentPage = async (params) => {
+  return await request.get({ url: `/infra/demo03-student/page`, params })
+}
+
+// 查询学生详情
+export const getDemo03Student = async (id: number) => {
+  return await request.get({ url: `/infra/demo03-student/get?id=` + id })
+}
+
+// 新增学生
+export const createDemo03Student = async (data: Demo03StudentVO) => {
+  return await request.post({ url: `/infra/demo03-student/create`, data })
+}
+
+// 修改学生
+export const updateDemo03Student = async (data: Demo03StudentVO) => {
+  return await request.put({ url: `/infra/demo03-student/update`, data })
+}
+
+// 删除学生
+export const deleteDemo03Student = async (id: number) => {
+  return await request.delete({ url: `/infra/demo03-student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportDemo03Student = async (params) => {
+  return await request.download({ url: `/infra/demo03-student/export-excel`, params })
+}
+
+// ==================== 子表(学生课程) ====================
+
+// 获得学生课程分页
+export const getDemo03CoursePage = async (params) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-course/page`, params })
+}
+// 新增学生课程
+export const createDemo03Course = async (data) => {
+  return await request.post({ url: `/infra/demo03-student/demo03-course/create`, data })
+}
+
+// 修改学生课程
+export const updateDemo03Course = async (data) => {
+  return await request.put({ url: `/infra/demo03-student/demo03-course/update`, data })
+}
+
+// 删除学生课程
+export const deleteDemo03Course = async (id: number) => {
+  return await request.delete({ url: `/infra/demo03-student/demo03-course/delete?id=` + id })
+}
+
+// 获得学生课程
+export const getDemo03Course = async (id: number) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-course/get?id=` + id })
+}
+
+// ==================== 子表(学生班级) ====================
+
+// 获得学生班级分页
+export const getDemo03GradePage = async (params) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-grade/page`, params })
+}
+// 新增学生班级
+export const createDemo03Grade = async (data) => {
+  return await request.post({ url: `/infra/demo03-student/demo03-grade/create`, data })
+}
+
+// 修改学生班级
+export const updateDemo03Grade = async (data) => {
+  return await request.put({ url: `/infra/demo03-student/demo03-grade/update`, data })
+}
+
+// 删除学生班级
+export const deleteDemo03Grade = async (id: number) => {
+  return await request.delete({ url: `/infra/demo03-student/demo03-grade/delete?id=` + id })
+}
+
+// 获得学生班级
+export const getDemo03Grade = async (id: number) => {
+  return await request.get({ url: `/infra/demo03-student/demo03-grade/get?id=` + id })
+}
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue b/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
new file mode 100644
index 00000000..29f1370d
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
@@ -0,0 +1,121 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-radio-group v-model="formData.sex">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <Editor v-model="formData.description" height="150px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  sex: undefined,
+  birthday: undefined,
+  description: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo03StudentApi.getDemo03Student(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as Demo03StudentApi.Demo03StudentVO
+    if (formType.value === 'create') {
+      await Demo03StudentApi.createDemo03Student(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo03StudentApi.updateDemo03Student(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    sex: undefined,
+    birthday: undefined,
+    description: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue b/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
new file mode 100644
index 00000000..de1c06de
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
@@ -0,0 +1,99 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="分数" prop="score">
+        <el-input v-model="formData.score" placeholder="请输入分数" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  name: undefined,
+  score: undefined
+})
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  score: [{ required: true, message: '分数不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, studentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.studentId = studentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo03StudentApi.getDemo03Course(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await Demo03StudentApi.createDemo03Course(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo03StudentApi.updateDemo03Course(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    score: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue b/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
new file mode 100644
index 00000000..7e06ee64
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
@@ -0,0 +1,126 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['infra:demo03-student:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="分数" align="center" prop="score" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo03-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <Demo03CourseForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import Demo03CourseForm from './Demo03CourseForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo03StudentApi.getDemo03CoursePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.studentId) {
+    message.error('请选择一个学生')
+    return
+  }
+  formRef.value.open(type, id, props.studentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo03StudentApi.deleteDemo03Course(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue b/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
new file mode 100644
index 00000000..abba0032
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
@@ -0,0 +1,99 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="班主任" prop="teacher">
+        <el-input v-model="formData.teacher" placeholder="请输入班主任" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  name: undefined,
+  teacher: undefined
+})
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, studentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.studentId = studentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await Demo03StudentApi.getDemo03Grade(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await Demo03StudentApi.createDemo03Grade(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await Demo03StudentApi.updateDemo03Grade(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    teacher: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue b/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
new file mode 100644
index 00000000..b12f1889
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
@@ -0,0 +1,126 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['infra:demo03-student:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="班主任" align="center" prop="teacher" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo03-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <Demo03GradeForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import Demo03GradeForm from './Demo03GradeForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo03StudentApi.getDemo03GradePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.studentId) {
+    message.error('请选择一个学生')
+    return
+  }
+  formRef.value.open(type, id, props.studentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo03StudentApi.deleteDemo03Grade(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+</script>
\ No newline at end of file
diff --git a/src/views/infra/demo/demo03/erp/index.vue b/src/views/infra/demo/demo03/erp/index.vue
new file mode 100644
index 00000000..8fdc7b42
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/index.vue
@@ -0,0 +1,243 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:demo03-student:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:demo03-student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      highlight-current-row
+      @current-change="handleCurrentChange"
+    >
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:demo03-student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:demo03-student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <Demo03StudentForm ref="formRef" @success="getList" />
+  <!-- 子表的列表 -->
+  <ContentWrap>
+    <el-tabs model-value="demo03Course">
+      <el-tab-pane label="学生课程" name="demo03Course">
+        <Demo03CourseList :student-id="currentRow.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班级" name="demo03Grade">
+        <Demo03GradeList :student-id="currentRow.id" />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as Demo03StudentApi from '@/api/infra/demo/demo03/erp'
+import Demo03StudentForm from './Demo03StudentForm.vue'
+import Demo03CourseList from './components/Demo03CourseList.vue'
+import Demo03GradeList from './components/Demo03GradeList.vue'
+
+defineOptions({ name: 'Demo03Student' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  sex: null,
+  description: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Demo03StudentApi.getDemo03StudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await Demo03StudentApi.deleteDemo03Student(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await Demo03StudentApi.exportDemo03Student(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 选中行操作 */
+const currentRow = ref({}) // 选中行
+const handleCurrentChange = (row) => {
+  currentRow.value = row
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
\ No newline at end of file