table.vue.ftl 和 form.vue.ftl 两个核心前端页面模板文件参考示例1

Vue3+TS企业级页面模板

以下是符合 现代 Vue 3 + TypeScript + Element Plus 开发规范、遵循 企业级中后台应用设计标准 的两个核心前端页面模板文件:

  • table.vue.ftl:列表页(含分页、查询、操作)
  • form.vue.ftl:表单页(含创建/编辑、校验、提交)

这两个模板均基于 TypeScript + Composition API + <script setup> 语法,集成 Element Plus 组件库,并与前文生成的 api.tstypes.ts 无缝对接,包含详尽的中文注释,可直接用于代码生成器。


✅ 1. table.vue.ftl(列表页模板)

<template>
  <div class="app-container">
    <!-- 查询条件区域 -->
    <el-card shadow="never" class="search-card">
      <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
        <!-- 动态生成查询字段 -->
        <#list table.fields as field>
        <#-- 仅包含适合查询的字段(String、Integer、Boolean) -->
        <#if !(field.name == 'id' || field.name == 'create_time' || field.name == 'update_time' || field.name == 'deleted' || field.name == 'password' || field.name == 'salt')>
        <#if field.propertyType == "String" || field.propertyType == "Integer" || field.propertyType == "Boolean">
        <el-form-item 
          label="${field.comment!}" 
          prop="${field.propertyName}"
        >
          <#if field.propertyType == "String">
          <el-input
            v-model="queryParams.${field.propertyName}"
            placeholder="请输入${field.comment!}"
            clearable
            @keyup.enter="handleSearch"
          />
          <#elseif field.propertyType == "Integer" || field.propertyType == "Long">
          <el-input
            v-model.number="queryParams.${field.propertyName}"
            placeholder="请输入${field.comment!}"
            clearable
            @keyup.enter="handleSearch"
          />
          <#elseif field.propertyType == "Boolean">
          <el-select
            v-model="queryParams.${field.propertyName}"
            placeholder="请选择${field.comment!}"
            clearable
          >
            <el-option :value="true" label="是" />
            <el-option :value="false" label="否" />
          </el-select>
          </#if>
        </el-form-item>
        </#if>
        </#if>
        </#list>

        <!-- 操作按钮 -->
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
          <el-button type="success" @click="handleCreate" v-permission="'${table.mappingPath}:add'">
            新增
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 数据表格区域 -->
    <el-card shadow="never" class="table-card">
      <el-table
        :data="tableData"
        v-loading="loading"
        border
        style="width: 100%"
        row-key="id"
      >
        <!-- 动态生成表格列 -->
        <#list table.fields as field>
        <#-- 跳过敏感字段和逻辑删除字段 -->
        <#if !(field.name == 'password' || field.name == 'salt' || field.name == 'deleted')>
        <el-table-column
          prop="${field.propertyName}"
          label="${field.comment!}"
          <#if field.propertyType == "String" && field.propertyName == 'description'>width="200"<#else>width="120"</#if>
        >
          <#-- 时间字段特殊处理 -->
          <#if field.propertyType == "LocalDateTime" || field.propertyType == "LocalDate">
          <template #default="scope">
            {{ formatDate(scope.row.${field.propertyName}) }}
          </template>
          <#-- 布尔字段转文字 -->
          <#elseif field.propertyType == "Boolean">
          <template #default="scope">
            {{ scope.row.${field.propertyName} ? '是' : '否' }}
          </template>
          </#if>
        </el-table-column>
        </#if>
        </#list>

        <!-- 操作列 -->
        <el-table-column label="操作" fixed="right" width="200">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="handleEdit(scope.row)"
              v-permission="'${table.mappingPath}:edit'"
            >
              编辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="handleDelete(scope.row)"
              v-permission="'${table.mappingPath}:delete'"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页组件 -->
      <el-pagination
        v-if="total > 0"
        :current-page="queryParams.page"
        :page-size="queryParams.size"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        class="pagination"
      />
    </el-card>
  </div>
</template>

<script setup lang="ts">
/**
 * ${table.comment!} 列表页
 *
 * 【功能说明】
 * 1. 支持条件查询、分页、排序
 * 2. 集成新增、编辑、删除操作
 * 3. 使用 Element Plus 组件库
 * 4. 权限控制(v-permission 指令)
 * 5. 时间格式化、布尔值转换等展示优化
 *
 * 【关键原则】
 * - 与后端 API 严格对齐(使用生成的 api.ts 和 types.ts)
 * - 查询参数与表格数据分离管理
 * - 操作前二次确认(删除)
 * - 加载状态反馈(v-loading)
 *
 * @author ${author}
 * @since ${date}
 */

// 导入 API 和类型
import { 
  list${entity}, 
  page${entity}, 
  delete${entity} 
} from '@/api/${table.mappingPath}';
import { 
  ${entity}Query, 
  ${entity}ListItemRes,
  PageRes 
} from '@/api/${table.mappingPath}/types';

// 导入工具函数
import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';

// 路由
const router = useRouter();

// 加载状态
const loading = ref(false);

// 查询参数(与 types.ts 中的 ${entity}Query 对齐)
const queryParams = reactive<${entity}Query>({
  page: 1,
  size: 10
  <#list table.fields as field>
  <#if !(field.name == 'id' || field.name == 'create_time' || field.name == 'update_time' || field.name == 'deleted' || field.name == 'password' || field.name == 'salt')>
  <#if field.propertyType == "String" || field.propertyType == "Integer" || field.propertyType == "Boolean">
  , ${field.propertyName}: undefined
  </#if>
  </#if>
  </#list>
});

// 表格数据
const tableData = ref<${entity}ListItemRes[]>([]);
const total = ref(0);

// 查询表单引用
const queryFormRef = ref();

/**
 * 格式化时间(ISO 8601 → 友好格式)
 */
const formatDate = (dateStr: string | null | undefined): string => {
  if (!dateStr) return '-';
  const date = new Date(dateStr);
  return date.toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  });
};

/**
 * 获取列表数据(分页)
 */
const fetchList = async () => {
  loading.value = true;
  try {
    const res = await page${entity}(queryParams);
    if (res.data.code === 200) {
      const pageData = res.data.data as PageRes<${entity}ListItemRes>;
      tableData.value = pageData.list;
      total.value = pageData.total;
    } else {
      ElMessage.error(res.data.message || '获取列表失败');
    }
  } catch (error) {
    console.error('获取列表失败:', error);
  } finally {
    loading.value = false;
  }
};

/**
 * 查询
 */
const handleSearch = () => {
  queryParams.page = 1; // 重置页码
  fetchList();
};

/**
 * 重置查询条件
 */
const handleReset = () => {
  queryFormRef.value?.resetFields();
  queryParams.page = 1;
  fetchList();
};

/**
 * 分页大小改变
 */
const handleSizeChange = (val: number) => {
  queryParams.size = val;
  fetchList();
};

/**
 * 当前页码改变
 */
const handleCurrentChange = (val: number) => {
  queryParams.page = val;
  fetchList();
};

/**
 * 新增
 */
const handleCreate = () => {
  router.push({ name: '${entity}Form', query: { mode: 'create' } });
};

/**
 * 编辑
 */
const handleEdit = (row: ${entity}ListItemRes) => {
  router.push({ 
    name: '${entity}Form', 
    query: { mode: 'edit', id: row.id.toString() } 
  });
};

/**
 * 删除
 */
const handleDelete = (row: ${entity}ListItemRes) => {
  ElMessageBox.confirm(
    `确定要删除 "${row.<#-- 找一个合适的显示字段,如 name --><#list table.fields as field><#if field.propertyType == "String" && field.propertyName != "password">${field.propertyName}<#break></#if></#list>}" 吗?`,
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(async () => {
    try {
      const res = await delete${entity}(row.id);
      if (res.data.code === 200) {
        ElMessage.success('删除成功');
        fetchList(); // 刷新列表
      } else {
        ElMessage.error(res.data.message || '删除失败');
      }
    } catch (error) {
      console.error('删除失败:', error);
    }
  });
};

// 页面加载时获取数据
onMounted(() => {
  fetchList();
});
</script>

<style scoped>
.app-container {
  padding: 20px;
}
.search-card {
  margin-bottom: 20px;
}
.table-card {
  :deep(.el-table) {
    margin-bottom: 20px;
  }
}
.pagination {
  display: flex;
  justify-content: flex-end;
}
</style>

✅ 2. form.vue.ftl(表单页模板)

<template>
  <div class="app-container">
    <el-card shadow="never">
      <template #header>
        <div class="card-header">
          <span>{{ mode === 'create' ? '新增${table.comment!}' : '编辑${table.comment!}' }}</span>
          <el-button type="primary" @click="handleSubmit" :loading="submitLoading">
            保存
          </el-button>
          <el-button @click="handleCancel">取消</el-button>
        </div>
      </template>

      <el-form
        :model="form"
        :rules="rules"
        ref="formRef"
        label-width="120px"
        :scroll-to-error="true"
      >
        <!-- 动态生成表单项 -->
        <#list table.fields as field>
        <#-- 跳过主键、基类字段和敏感字段 -->
        <#if !(field.name == 'id' || field.name == 'create_time' || field.name == 'update_time' || field.name == 'deleted' || field.name == 'password' || field.name == 'salt')>
        <el-form-item 
          label="${field.comment!}" 
          prop="${field.propertyName}"
        >
          <#if field.propertyType == "String">
          <el-input
            v-model="form.${field.propertyName}"
            placeholder="请输入${field.comment!}"
            clearable
          />
          <#elseif field.propertyType == "Integer" || field.propertyType == "Long">
          <el-input
            v-model.number="form.${field.propertyName}"
            placeholder="请输入${field.comment!}"
            clearable
          />
          <#elseif field.propertyType == "Boolean">
          <el-switch
            v-model="form.${field.propertyName}"
            active-text="是"
            inactive-text="否"
          />
          <#elseif field.propertyType == "LocalDateTime" || field.propertyType == "LocalDate">
          <el-date-picker
            v-model="form.${field.propertyName}"
            type="datetime"
            placeholder="选择${field.comment!}"
            format="YYYY-MM-DD HH:mm:ss"
            value-format="YYYY-MM-DD HH:mm:ss"
          />
          </#if>
        </el-form-item>
        </#if>
        </#list>
      </el-form>
    </el-card>
  </div>
</template>

<script setup lang="ts">
/**
 * ${table.comment!} 表单页(新增/编辑)
 *
 * 【功能说明】
 * 1. 支持创建和编辑两种模式
 * 2. 表单校验(与后端 DTO 校验规则对齐)
 * 3. 自动加载编辑数据
 * 4. 提交成功后返回列表页
 *
 * 【关键原则】
 * - 表单数据与 API DTO 结构一致
 * - 编辑模式需先获取详情再填充表单
 * - 提交前校验,提交时 loading 状态
 * - 路由参数控制模式(mode=create/edit)
 *
 * @author ${author}
 * @since ${date}
 */

// 导入 API 和类型
import { 
  create${entity}, 
  update${entity}, 
  get${entity}Detail 
} from '@/api/${table.mappingPath}';
import { 
  ${entity}CreateReq, 
  ${entity}UpdateReq,
  ${entity}DetailRes 
} from '@/api/${table.mappingPath}/types';

// 导入工具
import { ElMessage } from 'element-plus';
import { ref, reactive, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';

// 路由
const route = useRoute();
const router = useRouter();

// 表单引用
const formRef = ref();

// 提交状态
const submitLoading = ref(false);

// 表单模式(create / edit)
const mode = ref<'create' | 'edit'>('create');

// 表单数据(初始化为空)
const form = reactive<<#if table.fields?size gt 0>${entity}CreateReq<#else>Record<string, any></#if>>({
  <#list table.fields as field>
  <#if !(field.name == 'id' || field.name == 'create_time' || field.name == 'update_time' || field.name == 'deleted' || field.name == 'password' || field.name == 'salt')>
  ${field.propertyName}: 
  <#if field.propertyType == "String">''
  <#elseif field.propertyType == "Integer" || field.propertyType == "Long">null
  <#elseif field.propertyType == "Boolean">false
  <#elseif field.propertyType == "LocalDateTime" || field.propertyType == "LocalDate">null
  <#else>null</#if><#if field_index < table.fields?size - 1>,</#if>
  </#if>
  </#list>
});

// 表单校验规则(与后端 DTO 校验对齐)
const rules = {
  <#list table.fields as field>
  <#if !(field.name == 'id' || field.name == 'create_time' || field.name == 'update_time' || field.name == 'deleted' || field.name == 'password' || field.name == 'salt')>
  ${field.propertyName}: [
    {
      required: true,
      message: '${field.comment!}不能为空',
      trigger: 'blur'
    }
    <#if field.propertyType == "String">
    ,
    {
      max: 50,
      message: '${field.comment!}长度不能超过50个字符',
      trigger: 'blur'
    }
    </#if>
  ]<#if field_index < table.fields?size - 1>,</#if>
  </#if>
  </#list>
};

/**
 * 获取详情(编辑模式)
 */
const fetchDetail = async (id: number) => {
  try {
    const res = await get${entity}Detail(id);
    if (res.data.code === 200) {
      const detail = res.data.data as ${entity}DetailRes;
      // 填充表单(注意:时间字段需转换为字符串)
      Object.assign(form, detail);
    } else {
      ElMessage.error(res.data.message || '获取详情失败');
      handleCancel();
    }
  } catch (error) {
    console.error('获取详情失败:', error);
    handleCancel();
  }
};

/**
 * 提交表单
 */
const handleSubmit = async () => {
  await formRef.value?.validate(async (valid: boolean) => {
    if (valid) {
      submitLoading.value = true;
      try {
        let res;
        if (mode.value === 'create') {
          // 创建
          res = await create${entity}(form as ${entity}CreateReq);
        } else {
          // 编辑(需包含 ID)
          const updateData = {
            id: Number(route.query.id),
            ...form
          } as ${entity}UpdateReq;
          res = await update${entity}(updateData);
        }
        
        if (res.data.code === 200) {
          ElMessage.success(mode.value === 'create' ? '新增成功' : '更新成功');
          handleCancel(); // 返回列表页
        } else {
          ElMessage.error(res.data.message || (mode.value === 'create' ? '新增失败' : '更新失败'));
        }
      } catch (error) {
        console.error(mode.value === 'create' ? '新增失败:' : '更新失败:', error);
      } finally {
        submitLoading.value = false;
      }
    }
  });
};

/**
 * 取消并返回列表页
 */
const handleCancel = () => {
  router.push({ name: '${entity}List' });
};

// 监听路由参数变化(支持从列表页直接编辑)
watch(
  () => route.query,
  (newQuery) => {
    const { mode: queryMode, id } = newQuery;
    mode.value = (queryMode as 'create' | 'edit') || 'create';
    
    if (mode.value === 'edit' && id) {
      fetchDetail(Number(id));
    } else {
      // 重置表单
      formRef.value?.resetFields();
    }
  },
  { immediate: true }
);
</script>

<style scoped>
.app-container {
  padding: 20px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>

🔍 关键设计说明与最佳实践

1. 前后端无缝集成
  • 类型安全:使用生成的 types.ts 确保表单/表格字段与后端 DTO/VO 严格对齐
  • API 调用:直接调用生成的 api.ts 函数,避免手写 URL 和参数
  • 错误处理:统一处理后端返回的 code !== 200 情况
2. 用户体验优化
  • 加载状态v-loading 反馈异步操作
  • 时间格式化:ISO 字符串 → 本地友好格式
  • 布尔值转换true/false → “是/否”
  • 表单校验:前端校验 + 后端校验双重保障
  • 操作确认:删除前二次确认
3. 权限与安全
  • 权限指令v-permission 控制按钮显隐(需自行实现该指令)
  • 敏感字段过滤:模板中已排除 passwordsaltdeleted
  • 路由参数校验:编辑模式必须传 ID
4. 工程化规范
  • Composition API:使用 <script setup> + ref/reactive
  • TypeScript:完整类型定义,杜绝 any
  • 样式作用域<style scoped> 避免样式污染
  • 响应式布局:适配不同屏幕尺寸
5. 扩展性设计
  • 动态字段生成:根据数据库表结构自动生成表单/表格
  • 预留扩展点:注释中提供常见场景示例(如导出、批量操作)
  • 组件化思想:可进一步拆分为 SearchForm.vueActionColumn.vue

🛠️ 配套使用建议

(1) 路由配置(router.ts)
{
  path: '/${table.mappingPath}',
  name: '${entity}List',
  component: () => import('@/views/${table.mappingPath}/table.vue')
},
{
  path: '/${table.mappingPath}/form',
  name: '${entity}Form',
  component: () => import('@/views/${table.mappingPath}/form.vue')
}
(2) 权限指令实现(可选)
// directive/permission.ts
app.directive('permission', {
  mounted(el, binding) {
    const { value } = binding;
    const permissions = store.getters.permissions; // 从 Vuex/Pinia 获取用户权限
    if (value && !permissions.includes(value)) {
      el.parentNode?.removeChild(el);
    }
  }
});
(3) 项目结构
src/
├── api/
│   └── user/
│       ├── index.ts      ← api.ts
│       └── types.ts
├── views/
│   └── user/
│       ├── table.vue     ← 列表页
│       └── form.vue      ← 表单页
└── router/
    └── index.ts

✅ 这两份模板体现了 “开箱即用”“企业级健壮性” 的前端工程思想:

  • 自动生成完整 CRUD 页面
  • 类型安全贯穿始终
  • 用户体验细节到位
  • 安全与权限考虑周全
  • 代码结构清晰可维护

可作为团队标准模板,在代码生成流程中自动生成,大幅提升中后台应用开发效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值