26 KiB
26 KiB
前端代码生成参考
本文档包含 SpringBlade 开源版 Saber 前端各层的完整代码模板。基于 Vue 3 + Element Plus + Avue 技术栈,支持 Options API 和 Composition API 两种风格。
前端 API 路径在 Boot 单体与 Cloud 微服务下完全一致,都走
/blade-{module}/...。Boot 通过AppConstant.APPLICATION_XXX_NAME前缀对齐该路径,Cloud 则由 Gateway 按服务名blade-{module}自动路由。
目录
一、API 层
文件位置:src/api/{module}/{modelCode}.js
标准 CRUD API
import request from '@/axios';
/**
* 获取{中文名}列表(分页)
*/
export const getList = (current, size, params) => {
return request({
url: '/blade-{module}/{modelCode}/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
/**
* 获取{中文名}详情
*/
export const getDetail = (id) => {
return request({
url: '/blade-{module}/{modelCode}/detail',
method: 'get',
params: {
id,
},
});
};
/**
* 删除{中文名}
*/
export const remove = (ids) => {
return request({
url: '/blade-{module}/{modelCode}/remove',
method: 'post',
params: {
ids,
},
});
};
/**
* 新增{中文名}
*/
export const add = (row) => {
return request({
url: '/blade-{module}/{modelCode}/submit',
method: 'post',
data: row,
});
};
/**
* 修改{中文名}
*/
export const update = (row) => {
return request({
url: '/blade-{module}/{modelCode}/submit',
method: 'post',
data: row,
});
};
注意事项:
- 新增和修改共用
/submit接口(后端saveOrUpdate) remove的ids是逗号分隔的字符串- GET 请求用
params,POST 请求用data(请求体) - 如需加密传输,添加
cryptoToken: true和cryptoData: true
树形模式追加
/**
* 获取{中文名}树形结构
*/
export const getTree = (tenantId) => {
return request({
url: '/blade-{module}/{modelCode}/tree',
method: 'get',
params: {
tenantId,
},
});
};
导出接口
import { exportBlob } from '@/api/common';
/**
* 导出{中文名}数据
*/
export const exportData = (params) => {
return exportBlob(`/blade-{module}/{modelCode}/export-{modelCode}`, params);
};
二、Option 配置层
文件位置:src/option/{module}/{modelCode}.js
标准 CRUD 配置
export const tableOption = {
height: 'auto',
calcHeight: 30,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
viewBtn: true,
dialogClickModal: false,
column: [
// --- 文本输入框 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
search: true, // 是否为搜索字段
rules: [
{
required: true,
message: '请输入{字段中文名}',
trigger: 'blur',
},
],
},
// --- 数字输入 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'number',
align: 'right',
width: 100,
},
// --- 下拉选择(远程字典) ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'select',
search: true,
dicUrl: '/blade-system/dict/dictionary?code={dictCode}',
props: {
label: 'dictValue',
value: 'dictKey',
},
rules: [
{
required: true,
message: '请选择{字段中文名}',
trigger: 'change',
},
],
},
// --- 下拉选择(本地字典) ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'select',
dicData: [
{ label: '选项A', value: 1 },
{ label: '选项B', value: 2 },
],
},
// --- 开关 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'switch',
align: 'center',
width: 100,
dicData: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
value: 0,
},
// --- 单选 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'radio',
dicData: [
{ label: '选项A', value: 1 },
{ label: '选项B', value: 2 },
],
value: 1,
},
// --- 多选 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'checkbox',
dicData: [
{ label: '选项A', value: 1 },
{ label: '选项B', value: 2 },
],
},
// --- 日期选择 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
},
// --- 日期时间选择 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
// --- 日期范围搜索 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'datetimerange',
searchRange: true,
search: true,
hide: true, // 列表不显示
display: false, // 表单不显示
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
// --- 多行文本 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
type: 'textarea',
span: 24, // 占满整行
hide: true, // 列表不显示
minRows: 3,
},
// --- 富文本编辑器 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
component: 'avue-ueditor',
options: {
action: '/blade-resource/oss/endpoint/put-file',
props: {
res: 'data',
url: 'link',
},
},
span: 24,
hide: true,
minRows: 6,
},
// --- 仅列表显示,表单不显示 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
addDisplay: false,
editDisplay: false,
},
// --- 仅表单显示,列表不显示 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
hide: true,
},
// --- 自定义插槽 ---
{
label: '{字段中文名}',
prop: '{fieldName}',
slot: true, // 启用自定义渲染插槽
},
// --- 备注(隐藏在列表) ---
{
label: '备注',
prop: 'remark',
hide: true,
},
],
};
分组表单配置
用于复杂表单(如用户管理),将字段分组到多个 Tab 页:
export const tableOption = (safe) => {
// 自定义校验器(使用闭包访问组件实例)
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
callback();
}
};
return {
height: 'auto',
calcHeight: 150,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
viewBtn: true,
dialogType: 'drawer',
dialogWidth: 1000,
dialogClickModal: false,
// 列表使用 column
column: [
{ label: '字段A', prop: 'fieldA', search: true },
{ label: '字段B', prop: 'fieldB' },
],
// 表单使用 group(分组标签页)
group: [
{
label: '基础信息',
prop: 'baseInfo',
icon: 'el-icon-user-solid',
column: [
{ label: '字段A', prop: 'fieldA', rules: [{ required: true, message: '请输入', trigger: 'blur' }] },
{ label: '字段B', prop: 'fieldB', type: 'select', dicData: [] },
],
},
{
label: '详细信息',
prop: 'detailInfo',
icon: 'el-icon-s-order',
column: [
{ label: '字段C', prop: 'fieldC' },
{ label: '字段D', prop: 'fieldD', type: 'textarea', span: 24 },
],
},
],
};
};
列属性速查
| 属性 | 类型 | 说明 |
|---|---|---|
label |
String | 列标题/表单标签 |
prop |
String | 字段名 |
type |
String | 组件类型:input/select/radio/checkbox/switch/date/datetime/textarea/number/tree/upload |
search |
Boolean | 是否为搜索字段 |
hide |
Boolean | 列表中隐藏 |
display |
Boolean | 表单中隐藏(false=不显示) |
addDisplay |
Boolean | 新增表单中隐藏 |
editDisplay |
Boolean | 编辑表单中隐藏 |
addDisabled |
Boolean | 新增时禁用 |
editDisabled |
Boolean | 编辑时禁用 |
span |
Number | 表单栅格宽度(24=整行) |
slot |
Boolean | 启用自定义插槽 |
rules |
Array | 校验规则 |
dicUrl |
String | 远程字典 URL |
dicData |
Array | 本地字典数据 |
props |
Object | 字典映射 { label, value } |
value |
Any | 默认值 |
width |
Number | 列宽度 |
align |
String | 对齐方式:left/center/right |
overHidden |
Boolean | 超长文本省略显示 |
component |
String | 自定义组件名 |
searchRange |
Boolean | 搜索使用范围选择 |
format |
String | 显示格式 |
valueFormat |
String | 值格式 |
minRows |
Number | textarea 最小行数 |
三、Vue 页面 - Options API
文件位置:src/views/{module}/{modelCode}.vue
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
:page="page"
:permission="permissionList"
ref="crud"
v-model="form"
v-model:search="query"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
@click="$refs.crud.rowAdd()"
v-if="permission.{modelCode}_add"
>新 增</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
@click="handleDelete"
v-if="permission.{modelCode}_delete"
>删 除</el-button>
</template>
<!-- 自定义列插槽示例 -->
<template #{slotField}="{ row }">
<el-tag>{{ row.{slotField} }}</el-tag>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, add, update, remove } from '@/api/{module}/{modelCode}';
import { tableOption } from '@/option/{module}/{modelCode}';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
query: {},
loading: true,
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: tableOption,
data: [],
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.{modelCode}_add, false),
viewBtn: this.validData(this.permission.{modelCode}_view, false),
delBtn: this.validData(this.permission.{modelCode}_delete, false),
editBtn: this.validData(this.permission.{modelCode}_edit, false),
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
},
methods: {
rowSave(row, done, loading) {
add(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
}
);
},
rowUpdate(row, index, done, loading) {
update(row).then(
() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
done();
},
error => {
loading();
}
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return remove(row.id);
}).then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return remove(this.ids);
}).then(() => {
this.onLoad(this.page);
this.$message({
type: 'success',
message: '操作成功!',
});
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data;
});
}
done();
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
getList(
page.currentPage,
page.pageSize,
Object.assign(params, this.query)
).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
this.selectionClear();
});
},
},
};
</script>
四、Vue 页面 - Composition API
文件位置:src/views/{module}/{modelCode}.vue
<template>
<basic-container>
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
:page="page"
:permission="permissionList"
ref="crud"
v-model="form"
v-model:search="query"
:before-open="beforeOpen"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
@click="crud.rowAdd()"
v-if="permission.{modelCode}_add"
>新 增</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
@click="handleDelete"
v-if="permission.{modelCode}_delete"
>删 除</el-button>
</template>
</avue-crud>
</basic-container>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { useStore } from 'vuex';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getList, getDetail, add, update, remove } from '@/api/{module}/{modelCode}';
import { tableOption } from '@/option/{module}/{modelCode}';
const store = useStore();
const crud = ref(null);
// 数据
const form = ref({});
const query = ref({});
const loading = ref(true);
const data = ref([]);
const selectionList = ref([]);
const option = reactive(tableOption);
const page = ref({
pageSize: 10,
currentPage: 1,
total: 0,
});
// 权限
const permission = computed(() => store.getters.permission);
const permissionList = computed(() => {
const validData = (value, defaultVal = false) => (value ? true : defaultVal);
return {
addBtn: validData(permission.value.{modelCode}_add),
viewBtn: validData(permission.value.{modelCode}_view),
delBtn: validData(permission.value.{modelCode}_delete),
editBtn: validData(permission.value.{modelCode}_edit),
};
});
const ids = computed(() => {
return selectionList.value.map(ele => ele.id).join(',');
});
// 方法
const rowSave = (row, done, loading) => {
add(row).then(
() => {
onLoad(page.value);
ElMessage.success('操作成功!');
done();
},
error => {
loading();
}
);
};
const rowUpdate = (row, index, done, loading) => {
update(row).then(
() => {
onLoad(page.value);
ElMessage.success('操作成功!');
done();
},
error => {
loading();
}
);
};
const rowDel = (row) => {
ElMessageBox.confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return remove(row.id);
}).then(() => {
onLoad(page.value);
ElMessage.success('操作成功!');
});
};
const handleDelete = () => {
if (selectionList.value.length === 0) {
ElMessage.warning('请选择至少一条数据');
return;
}
ElMessageBox.confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return remove(ids.value);
}).then(() => {
onLoad(page.value);
ElMessage.success('操作成功!');
crud.value.toggleSelection();
});
};
const beforeOpen = (done, type) => {
if (['edit', 'view'].includes(type)) {
getDetail(form.value.id).then(res => {
form.value = res.data.data;
});
}
done();
};
const searchChange = (params, done) => {
query.value = params;
page.value.currentPage = 1;
onLoad(page.value, params);
done();
};
const searchReset = () => {
query.value = {};
onLoad(page.value);
};
const selectionChange = (list) => {
selectionList.value = list;
};
const currentChange = (currentPage) => {
page.value.currentPage = currentPage;
};
const sizeChange = (pageSize) => {
page.value.pageSize = pageSize;
};
const refreshChange = () => {
onLoad(page.value, query.value);
};
const onLoad = (page, params = {}) => {
loading.value = true;
getList(
page.currentPage,
page.pageSize,
Object.assign(params, query.value)
).then(res => {
const resData = res.data.data;
page.total = resData.total;
data.value = resData.records;
loading.value = false;
selectionList.value = [];
});
};
</script>
Options API vs Composition API 转换对照
| Options API | Composition API |
|---|---|
data() { return { form: {} } } |
const form = ref({}) |
computed: { ids() { ... } } |
const ids = computed(() => { ... }) |
methods: { rowSave() { ... } } |
const rowSave = () => { ... } |
this.form |
form.value(模板中直接用 form) |
this.$refs.crud |
crud.value |
this.$message.success(...) |
ElMessage.success(...) |
this.$confirm(...) |
ElMessageBox.confirm(...) |
...mapGetters(['permission']) |
const permission = computed(() => store.getters.permission) |
mounted() { ... } |
onMounted(() => { ... }) |
watch: { ... } |
watch(source, callback) |
五、树形结构模式
Option 配置(追加树形父节点字段)
// 在 column 中添加树形父节点字段
{
label: '上级{中文名}',
prop: 'parentId',
type: 'tree',
dicData: [], // 动态加载
hide: true,
value: 0, // 默认值为顶级
props: {
label: 'title',
value: 'value',
children: 'children',
},
},
Vue 页面(树形数据加载)
在 onLoad 之后加载树形数据并更新列配置:
// Options API
methods: {
onLoad(page, params = {}) {
this.loading = true;
getList(page.currentPage, page.pageSize, Object.assign(params, this.query)).then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.loading = false;
// 加载树形数据
getTree().then(treeRes => {
const column = this.findObject(this.option.column, 'parentId');
column.dicData = treeRes.data.data;
});
});
},
},
// Composition API
const onLoad = (page, params = {}) => {
loading.value = true;
getList(page.currentPage, page.pageSize, Object.assign(params, query.value)).then(res => {
const resData = res.data.data;
page.total = resData.total;
data.value = resData.records;
loading.value = false;
getTree().then(treeRes => {
const column = findObject(option.column, 'parentId');
column.dicData = treeRes.data.data;
});
});
};
表格树形展示
在 avue-crud 模板中使用树形属性:
<avue-crud
:option="option"
:data="data"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
六、主子表模式
API 层(主表 + 子表)
// 主表 API(同标准 CRUD)
export const getList = (current, size, params) => { ... };
export const add = (row) => { ... };
export const update = (row) => { ... };
export const remove = (ids) => { ... };
// 子表 API
export const getSubList = (current, size, {fkFieldName}, params) => {
return request({
url: '/blade-{module}/{subModelCode}/list',
method: 'get',
params: {
...params,
current,
size,
{fkFieldName},
},
});
};
export const addSub = (row) => {
return request({
url: '/blade-{module}/{subModelCode}/submit',
method: 'post',
data: row,
});
};
export const updateSub = (row) => {
return request({
url: '/blade-{module}/{subModelCode}/submit',
method: 'post',
data: row,
});
};
export const removeSub = (ids) => {
return request({
url: '/blade-{module}/{subModelCode}/remove',
method: 'post',
params: { ids },
});
};
Vue 页面(主子表 Drawer 模式)
<template>
<basic-container>
<!-- 主表 -->
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
:page="page"
:permission="permissionList"
ref="crud"
v-model="form"
@row-del="rowDel"
@row-update="rowUpdate"
@row-save="rowSave"
@on-load="onLoad"
>
<template #menu="{ row }">
<el-button text type="primary" @click="openSub(row)">子表</el-button>
</template>
</avue-crud>
<!-- 子表抽屉 -->
<el-drawer
title="{子表中文名}"
v-model="subBox"
size="60%"
append-to-body
>
<avue-crud
:option="subOption"
:table-loading="subLoading"
:data="subData"
:page="subPage"
ref="crudSub"
v-model="subForm"
@row-del="rowDelSub"
@row-update="rowUpdateSub"
@row-save="rowSaveSub"
@on-load="onLoadSub"
/>
</el-drawer>
</basic-container>
</template>
<script>
export default {
data() {
return {
// 主表数据
form: {},
data: [],
loading: true,
page: { pageSize: 10, currentPage: 1, total: 0 },
option: tableOption,
// 子表数据
subBox: false,
subForm: {},
subData: [],
subLoading: true,
subPage: { pageSize: 10, currentPage: 1, total: 0 },
subOption: subTableOption,
mainId: '',
};
},
methods: {
openSub(row) {
this.mainId = row.id;
this.subBox = true;
this.onLoadSub(this.subPage);
},
rowSaveSub(row, done, loading) {
row.{fkFieldName} = this.mainId;
addSub(row).then(
() => {
this.onLoadSub(this.subPage);
this.$message.success('操作成功!');
done();
},
error => {
loading();
}
);
},
rowUpdateSub(row, index, done, loading) {
updateSub(row).then(
() => {
this.onLoadSub(this.subPage);
this.$message.success('操作成功!');
done();
},
error => {
loading();
}
);
},
rowDelSub(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
return removeSub(row.id);
}).then(() => {
this.onLoadSub(this.subPage);
this.$message.success('操作成功!');
});
},
onLoadSub(page, params = {}) {
this.subLoading = true;
getSubList(
page.currentPage,
page.pageSize,
this.mainId,
params
).then(res => {
const data = res.data.data;
this.subPage.total = data.total;
this.subData = data.records;
this.subLoading = false;
});
},
// ... 主表方法同标准 CRUD
},
};
</script>
附:常用工具函数
数组与逗号字符串互转
后端用逗号字符串存储多选值(如 deptId="1,2,3"),前端需要转换:
import func from '@/utils/func';
// 提交时:数组 → 逗号字符串
row.deptId = func.join(row.deptId);
// 回显时:逗号字符串 → 数组
this.form.deptId = func.split(this.form.deptId);
导出功能
import NProgress from 'nprogress';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
const handleExport = () => {
NProgress.start();
exportBlob(`/blade-{module}/{modelCode}/export-{modelCode}?${qs.stringify(this.query)}`).then(res => {
downloadXls(res.data, `{中文名}数据${this.$dayjs().format('YYYY-MM-DD')}.xlsx`);
NProgress.done();
});
};
辅助方法 findObject / findColumn
// 在 option 的 column 中查找指定 prop 的列
const column = this.findObject(this.option.column, 'fieldName');
// 在 group 中查找
const column = this.findColumn(this.option.group, 'fieldName');
这两个是全局挂载的工具方法(在 main.js 中注册)。