SpringBlade/.claude/skills/blade-design/references/frontend.md
2026-04-17 18:50:28 +08:00

1170 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 前端代码生成参考
本文档包含 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 层](#一api-层)
- [二、Option 配置层](#二option-配置层)
- [三、Vue 页面 - Options API](#三vue-页面---options-api)
- [四、Vue 页面 - Composition API](#四vue-页面---composition-api)
- [五、树形结构模式](#五树形结构模式)
- [六、主子表模式](#六主子表模式)
---
## 一、API 层
文件位置:`src/api/{module}/{modelCode}.js`
### 标准 CRUD API
```javascript
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`
### 树形模式追加
```javascript
/**
* 获取{中文名}树形结构
*/
export const getTree = (tenantId) => {
return request({
url: '/blade-{module}/{modelCode}/tree',
method: 'get',
params: {
tenantId,
},
});
};
```
### 导出接口
```javascript
import { exportBlob } from '@/api/common';
/**
* 导出{中文名}数据
*/
export const exportData = (params) => {
return exportBlob(`/blade-{module}/{modelCode}/export-{modelCode}`, params);
};
```
---
## 二、Option 配置层
文件位置:`src/option/{module}/{modelCode}.js`
### 标准 CRUD 配置
```javascript
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 页:
```javascript
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`
```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`
```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 配置(追加树形父节点字段)
```javascript
// 在 column 中添加树形父节点字段
{
label: '上级{中文名}',
prop: 'parentId',
type: 'tree',
dicData: [], // 动态加载
hide: true,
value: 0, // 默认值为顶级
props: {
label: 'title',
value: 'value',
children: 'children',
},
},
```
### Vue 页面(树形数据加载)
`onLoad` 之后加载树形数据并更新列配置:
```javascript
// 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 模板中使用树形属性:
```vue
<avue-crud
:option="option"
:data="data"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
```
---
## 六、主子表模式
### API 层(主表 + 子表)
```javascript
// 主表 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 模式)
```vue
<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"),前端需要转换:
```javascript
import func from '@/utils/func';
// 提交时:数组 → 逗号字符串
row.deptId = func.join(row.deptId);
// 回显时:逗号字符串 → 数组
this.form.deptId = func.split(this.form.deptId);
```
### 导出功能
```javascript
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
```javascript
// 在 option 的 column 中查找指定 prop 的列
const column = this.findObject(this.option.column, 'fieldName');
// 在 group 中查找
const column = this.findColumn(this.option.group, 'fieldName');
```
这两个是全局挂载的工具方法(在 `main.js` 中注册)。