Files
djangoadmin-wiki/DjangoAdmin_FastAPI_AntdVue/zh/volume2/fe-view.md
T
2026-09-14 09:44:33 +08:00

520 lines
13 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.
# 前端页面视图
前端页面视图是用户直接交互的界面。项目使用 Vue3 + AntDesign Vue + Vite 构建。一个完整的 CRUD 页面由 4 个文件组成:
```
ui/src/views/system/position/
├── index.vue # 主页面:搜索 + 表格 + 操作
├── edit.vue # 编辑弹窗:新增/编辑表单
├── columns.ts # 表格列定义
└── querySchemas.ts # 搜索表单 Schema
```
## 1. 主页面 `index.vue`
主页面是模块的入口页面,包含搜索表单、数据表格和操作按钮。
### 完整代码
```vue
<template>
<PageWrapper>
<a-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
</a-card>
<a-card :bordered="false" class="proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
:actionColumn="actionColumn"
@selection-change="onSelectionChange"
>
<template #tableTitle>
<a-space>
<a-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
<template #icon>
<PlusOutlined />
</template>
添加岗位
</a-button>
<a-button
type="primary"
danger
@click="handleDelete()"
:disabled="!selectionData.length"
v-perm="['sys:position:batchDelete']"
>
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</a-space>
</template>
</BasicTable>
</a-card>
<editDialog
v-if="editVisible"
:positionId="positionId"
v-model:visible="editVisible"
@success="reloadTable('noRefresh')"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { schemas } from './querySchemas';
import { useForm } from '@/components/Form/index';
import { TableAction } from '@/components/Table';
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
import { columns } from './columns';
import { Modal, message } from 'ant-design-vue';
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
const positionId = ref(0);
const editVisible = ref(false);
const selectionData = ref([]);
const tableRef = ref();
/**
* 定义查询参数
*/
const formParams = reactive({
name: '',
status: '',
});
/**
* 定义操作栏
*/
const actionColumn = reactive({
width: 200,
label: '操作',
prop: 'action',
fixed: 'right',
render(record) {
return h(TableAction, {
style: 'button',
actions: [
{
label: '编辑',
icon: 'Edit',
type: 'warning',
onClick: handleEdit.bind(null, record),
auth: ['sys:position:update'],
},
{
label: '删除',
icon: 'Delete',
type: 'danger',
onClick: handleDelete.bind(null, record),
auth: ['sys:position:delete'],
},
],
});
},
});
/**
* 加载数据列表
*/
const loadDataTable = async (res: any) => {
const result = await getPositionList({ ...formParams, ...res });
return result;
};
/**
* 刷新数据列表
*/
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
/**
* 注册搜索表单
*/
const [register, {}] = useForm({
rowProps: { gutter: [16, 0] },
colProps: {
xs: 24,
sm: 24,
md: 12,
lg: 8,
xl: 6,
},
labelCol: { span: 6, offset: 0 },
schemas,
});
/**
* 执行提交表单
*/
function handleSubmit(values: Recordable) {
handleReset();
for (const key in values) {
formParams[key] = values[key];
}
reloadTable();
}
/**
* 执行重置
*/
function handleReset() {
for (const key in formParams) {
formParams[key] = '';
}
}
/**
* 执行添加
*/
const handleAdd = async () => {
positionId.value = 0;
await nextTick();
editVisible.value = true;
};
/**
* 执行编辑
*/
const handleEdit = async (id: number) => {
positionId.value = id;
await nextTick();
editVisible.value = true;
};
/**
* 执行删除(单条 + 批量)
*/
async function handleDelete(id?: number) {
Modal.confirm({
title: '提示',
content: '确定要删除?',
onOk: async () => {
id ? await positionDelete(id) : await positionBatchDelete(selectionData.value);
message.success('删除成功');
reloadTable();
},
});
}
/**
* 选项发生变化
*/
function onSelectionChange(value) {
selectionData.value = value;
}
</script>
```
### 代码解析
**页面结构**:`PageWrapper` 包裹两个 `a-card`,上方是搜索表单,下方是数据表格。
**搜索表单**:通过 `useForm` 注册,Schema 从 `querySchemas.ts` 导入。`submitOnReset: true` 表示重置时自动触发查询。
**数据表格**:通过 `BasicTable` 组件渲染,`request` 属性绑定数据加载函数,`columns` 从 `columns.ts` 导入。
**操作栏**:通过 `actionColumn` 的 `render` 函数使用 `h()` 渲染 `TableAction`,支持权限控制(`auth`)。
**编辑弹窗**:使用 `defineAsyncComponent` 懒加载 `edit.vue`,通过 `v-model:visible` 控制显示,`positionId` 传入编辑记录 ID(0 表示新增)。
**删除逻辑**:单条删除传入 `record`,批量删除从 `selectionData` 取已选 ID。共用同一个 `handleDelete` 方法。
## 2. 编辑弹窗 `edit.vue`
编辑弹窗是独立组件,负责新增和编辑操作的表单展示与提交。
### 完整代码
```vue
<template>
<a-modal
v-model:visible="props.visible"
:title="props.positionId ? '编辑' : '新增'"
width="500px"
@cancel="dialogClose"
>
<a-form
class="ls-form"
ref="formRef"
:model="formData"
:label-col="{ style: { width: '85px' } }"
>
<a-form-item
label="岗位名称"
name="name"
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
>
<a-input v-model:value="formData.name" placeholder="请输入岗位名称" allow-clear />
</a-form-item>
<a-form-item label="岗位状态" name="status">
<a-radio-group v-model:value="formData.status" name="status">
<a-radio :value="1">正常</a-radio>
<a-radio :value="2">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="formData.sort" />
</a-form-item>
</a-form>
<template #footer>
<span class="dialog-footer">
<a-button @click="dialogClose">取消</a-button>
<a-button :loading="subLoading" type="primary" @click="submit"> 确定 </a-button>
</span>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
import { onMounted, reactive, shallowRef } from 'vue';
import { message } from 'ant-design-vue';
import { useLockFn } from '@/utils/useLockFn';
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
/**
* 定义表单参数
*/
const formData = reactive({
id: '',
name: '',
status: 1,
sort: 0,
});
/**
* 定义接收的参数
*/
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
positionId: {
type: Number,
required: true,
default: 0,
},
});
/**
* 执行提交表单
*/
const handleSubmit = async () => {
await formRef.value?.validate();
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message.success('操作成功');
emit('update:visible', false);
emit('success');
};
/**
* 关闭窗体
*/
const dialogClose = () => {
emit('update:visible', false);
};
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
/**
* 设置表单数据(编辑模式)
*/
const setFormData = async () => {
const data = await getPositionDetail(props.positionId);
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
formData[key] = data[key];
}
}
};
/**
* 钩子函数
*/
onMounted(() => {
if (props.positionId) {
setFormData();
}
});
</script>
```
### 代码解析
**Props 接收**:
- `visible`:控制弹窗显示/隐藏,通过 `v-model:visible` 双向绑定
- `positionId`:编辑记录的 ID,为 0 时表示新增模式
**表单数据**:使用 `reactive` 定义,字段与后端 Schema 对应。
**新增/编辑判断**:`props.positionId ? positionUpdate : positionAdd`,通过 ID 是否为 0 区分。
**防重复提交**:`useLockFn` 包装提交函数,点击后按钮变为 loading 状态,防止重复提交。
**数据回填**:`onMounted` 时判断如果是编辑模式,调用 `getPositionDetail` 获取数据并填充表单。
**事件通信**:
- `emit('update:visible', false)`:关闭弹窗
- `emit('success')`:通知父页面刷新表格
## 3. 表格列定义 `columns.ts`
定义表格的列配置,包括列名、字段名、宽度和自定义渲染。
### 完整代码
```typescript
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
export const columns = [
{
type: 'selection',
},
{
title: 'ID',
dataIndex: 'id',
fixed: 'left',
width: 50,
},
{
title: '岗位名称',
dataIndex: 'name',
minWidth: 100,
},
{
title: '岗位状态',
dataIndex: 'status',
minWidth: 100,
customRender: ({ record }) => {
return h(
Tag,
{
color: record.status == 1 ? 'success' : 'error',
},
() => (record.status == 1 ? '正常' : '停用'),
);
},
},
{
title: '排序',
dataIndex: 'sort',
minWidth: 100,
},
{
title: '创建人',
dataIndex: 'createUser',
minWidth: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
},
];
```
### 代码解析
**选择列**:`type: 'selection'` 自动添加复选框列,配合 `@selection-change` 事件实现批量选择。
**固定列**:`fixed: 'left'` 固定在左侧,`fixed: 'right'` 固定在右侧(操作列)。
**自定义渲染**:`customRender({ record })` 使用 Vue 的 `h()` 函数创建虚拟 DOM。状态列将 `1/2` 渲染为绿色/红色标签。
**宽度设置**:
- `width`:固定宽度
- `minWidth`:最小宽度,可自适应拉伸
## 4. 搜索表单 `querySchemas.ts`
定义搜索表单的字段配置,使用项目封装的 `FormSchema` 类型。
### 完整代码
```typescript
import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
field: 'name',
component: 'Input',
label: '岗位名称',
componentProps: {
placeholder: '请输入岗位名称',
},
},
{
field: 'status',
component: 'Select',
label: '状态',
componentProps: {
placeholder: '请选择状态',
allowClear: true,
options: [
{
label: '正常',
value: '1',
},
{
label: '禁用',
value: '2',
},
],
},
},
];
```
### 代码解析
**FormSchema 字段**:
| 字段 | 说明 |
|:----|:----|
| `field` | 字段名,与后端查询参数对应 |
| `component` | 表单组件类型:`Input` / `Select` / `RangePicker` 等 |
| `label` | 标签文本 |
| `componentProps` | 组件属性,透传给 AntDesign Vue 组件 |
**Select 组件**:`options` 定义下拉选项,`allowClear: true` 允许清空选择。
## 文件关系图
```
index.vue(主页面)
├── import schemas from './querySchemas' ← 搜索表单配置
├── import columns from './columns' ← 表格列配置
├── import editDialog from './edit.vue' ← 编辑弹窗组件
└── import API from '@/api/system/position' ← 接口请求
edit.vue(编辑弹窗)
└── import API from '@/api/system/position' ← 接口请求
```
## 开发要点
1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下
2. **4 个文件各司其职**:主页面、编辑弹窗、列定义、搜索 Schema
3. **编辑弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面
4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、操作按钮
5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段
6. **新增/编辑通过 `positionId` 区分**:0 为新增,非 0 为编辑
7. **防重复提交使用 `useLockFn`**:提交时按钮 loading
## 总结
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。