Files
djangoadmin-wiki/DjangoAdmin_Django_NaiveVue/zh/volume2/fe-view.md
T
2026-09-12 23:12:15 +08:00

431 lines
15 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 + ElementPlus + Vite 构建。一个完整的 CRUD 页面由 5 个文件组成:
```
ui/src/views/tool/example/
├── index.vue # 主页面:搜索 + 表格 + 操作
├── edit.vue # 编辑弹窗:新增/编辑表单
├── detail.vue # 详情弹窗:查看记录详情
├── columns.ts # 表格列定义
└── querySchemas.ts # 搜索表单 Schema
```
## 1. 主页面 index.vue
主页面是模块的入口页面,包含搜索表单、数据表格和操作按钮。
### 核心结构
```vue
<template>
<PageWrapper>
<!-- 搜索表单卡片 -->
<el-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
</el-card>
<!-- 数据表格卡片 -->
<el-card :bordered="false" class="proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
:actionColumn="actionColumn"
@selection-change="onSelectionChange"
>
<template #tableTitle>
<el-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
添加案例
</el-button>
<el-button type="danger" @click="handleDelete()" :disabled="!selectionData.length">
批量删除
</el-button>
</template>
</BasicTable>
</el-card>
<!-- 编辑对话框 -->
<editDialog v-if="editVisible" :id="currentId" v-model:visible="editVisible" @success="reloadTable('noRefresh')" />
<!-- 详情对话框 -->
<detailDialog v-if="detailVisible" :id="currentId" v-model:visible="detailVisible" @success="reloadTable('noRefresh')" />
</PageWrapper>
</template>
```
### 核心逻辑
```vue
<script lang="ts" setup>
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
import { schemas } from './querySchemas';
import { useForm } from '@/components/Form/index';
import { TableAction } from '@/components/Table';
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
import { columns } from './columns';
// 动态导入弹窗组件
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
// 响应式状态
const currentId = ref(0);
const editVisible = ref(false);
const detailVisible = ref(false);
const selectionData = ref([]);
const tableRef = ref();
// 查询参数
const formParams = reactive({ name: '', type: '', status: '' });
// 操作栏配置
const actionColumn = reactive({
width: 300,
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:example:update'] },
{ label: '详情', icon: 'View', type: '', onClick: handleDetail.bind(null, record), auth: ['sys:example:detail'] },
{ label: '删除', icon: 'Delete', type: 'danger', onClick: handleDelete.bind(null, record), auth: ['sys:example:delete'] },
],
});
},
});
// 加载数据
const loadDataTable = async (params: any) => {
return await getExamplePage({ ...formParams, ...params });
};
// 注册搜索表单
const [register, {}] = useForm({
labelWidth: 80,
layout: 'horizontal',
colProps: { span: 6 },
submitOnReset: true,
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 () => { currentId.value = 0; await nextTick(); editVisible.value = true; };
// 编辑
const handleEdit = async (record: Recordable) => { currentId.value = record.row.id; await nextTick(); editVisible.value = true; };
// 详情
const handleDetail = async (record: Recordable) => { currentId.value = record.row.id; await nextTick(); detailVisible.value = true; };
// 删除
async function handleDelete(record: Recordable) {
let ids = [];
if (!record) {
ids = selectionData.value.map(({ id }) => id);
if (!ids.length) { message('请选择要删除的数据', 'warning'); return; }
}
await confirm('确定要删除选中的案例吗?');
record ? await exampleDelete(record.row.id) : await exampleBatchDelete(ids);
message('删除成功');
reloadTable();
}
</script>
```
### 关键点
- **PageWrapper**:页面容器,提供统一的页面布局
- **BasicForm**:搜索表单组件,通过 `useForm` 注册
- **BasicTable**:数据表格组件,内置分页功能
- **TableAction**:操作栏组件,支持权限控制
- **defineAsyncComponent**:懒加载弹窗组件,优化首屏加载
- **v-perm**:权限指令,控制按钮显示
## 2. 编辑弹窗 edit.vue
编辑弹窗是独立组件,负责新增和编辑操作的表单展示与提交。
### 核心结构
```vue
<template>
<el-dialog
v-model="props.visible"
:title="props.id ? '编辑案例' : '新增案例'"
width="500"
:close-on-click-modal="false"
:before-close="dialogClose"
>
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
<el-form-item label="案例名称" prop="name" :rules="{ required: true, message: '请输入案例名称', trigger: 'blur' }">
<el-input v-model="formData.name" placeholder="请输入案例名称" clearable />
</el-form-item>
<el-form-item label="案例图片" prop="avatar">
<UploadImg v-model:image-url="formData.avatar" name="example" />
</el-form-item>
<el-form-item label="案例类型" prop="type" :rules="{ required: true, message: '请选择案例类型', trigger: 'change' }">
<el-select v-model="formData.type" placeholder="请选择" clearable filterable>
<el-option v-for="item in optionsData.typeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="案例状态" prop="status" :rules="{ required: true, message: '请选择状态', trigger: 'change' }">
<el-radio-group v-model="formData.status">
<el-radio :value="1">正常</el-radio>
<el-radio :value="2">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort" :rules="{ required: true, message: '请输入排序', trigger: 'blur' }">
<el-input-number v-model="formData.sort" :min="0" :max="99999" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogClose">取消</el-button>
<el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
</template>
</el-dialog>
</template>
```
### 核心逻辑
```vue
<script lang="ts" setup>
import { getExampleDetail, exampleAdd, exampleUpdate } from '@/api/tool/example';
import { onMounted, reactive, shallowRef, watch } from 'vue';
import { useLockFn } from '@/utils/useLockFn';
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
const formData = reactive({
id: '', name: '', avatar: '', type: 0, status: 0, sort: 0,
});
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
id: { type: Number, required: true, default: 0 },
});
// 提交表单
const handleSubmit = async () => {
await formRef.value?.validate();
props.id ? await exampleUpdate(formData) : await exampleAdd(formData);
message('操作成功');
emit('update:visible', false);
emit('success');
};
// 防重复提交
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
// 编辑时加载数据
const setFormData = async () => {
const data = await getExampleDetail(props.id);
for (const key in formData) {
if (data[key] != null && data[key] != undefined) { formData[key] = data[key]; }
}
};
onMounted(() => {
if (props.id) { setFormData(); } else { resetForm(); }
});
</script>
```
### 关键点
- **Props 接收**:`visible` 控制弹窗显示,`id` 区分新增/编辑模式
- **新增/编辑判断**:`props.id ? exampleUpdate : exampleAdd`
- **防重复提交**:`useLockFn` 包装提交函数,点击后按钮变为 loading 状态
- **数据回填**:编辑模式下调用 `getExampleDetail` 获取数据并填充表单
- **事件通信**:`emit('update:visible')` 关闭弹窗,`emit('success')` 通知刷新
## 3. 详情弹窗 detail.vue
详情弹窗用于展示记录的详细信息,使用 ElementPlus 的 `el-descriptions` 组件。
### 核心结构
```vue
<template>
<el-dialog v-model="props.visible" title="案例详情" width="750" :close-on-click-modal="false">
<el-descriptions column="2" border>
<el-descriptions-item label="案例名称:">{{ formData.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="案例图片:">
<el-image v-if="formData.avatar" :src="formData.avatar" :preview-src-list="[formData.avatar]" fit="cover" style="width: 80px; height: 80px" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="案例类型:">{{ getChoiceLabel('type', formData.type) }}</el-descriptions-item>
<el-descriptions-item label="案例状态:">{{ getChoiceLabel('status', formData.status) }}</el-descriptions-item>
<el-descriptions-item label="排序:">{{ formData.sort || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="dialogClose">关闭</el-button>
</template>
</el-dialog>
</template>
```
### 关键点
- **el-descriptions**:ElementPlus 的描述列表组件,用于展示详情
- **getChoiceLabel**:将枚举值转换为中文标签
- **图片预览**:使用 `el-image` 的 `preview-src-list` 实现图片预览
## 4. 表格列定义 columns.ts
定义表格的列配置,包括列名、字段名、宽度和自定义渲染。
### 完整代码
```typescript
import { h } from 'vue';
import { ElAvatar, ElTag } from 'element-plus';
export const columns = [
{ type: 'selection' },
{ label: 'ID', prop: 'id', fixed: 'left', width: 50 },
{ label: '案例名称', prop: 'name', minWidth: 120 },
{
label: '案例图片', prop: 'avatar', minWidth: 120,
render(record) {
return h(ElAvatar, { size: 48, src: record.row.avatar, shape: 'square', fit: 'fill' });
},
},
{
label: '案例类型', prop: 'type', minWidth: 100,
render(record) {
const statusMap = {
1: { text: '类型1', type: 'success' },
2: { text: '类型2', type: 'danger' },
3: { text: '类型3', type: 'warning' },
4: { text: '类型4', type: 'info' },
};
const status = statusMap[record.row.type];
return h(ElTag, { type: status?.type || 'info' }, { default: () => status?.text || record.row.type });
},
},
{
label: '案例状态', prop: 'status', minWidth: 100,
render(record) {
const statusMap = {
1: { text: '正常', type: 'success' },
2: { text: '禁用', type: 'danger' },
};
const status = statusMap[record.row.status];
return h(ElTag, { type: status?.type || 'info' }, { default: () => status?.text || record.row.status });
},
},
{ label: '排序', prop: 'sort', minWidth: 100 },
{ label: '创建人', prop: 'createUser', minWidth: 100 },
{ label: '创建时间', prop: 'createTime', width: 180 },
];
```
### 关键点
- **type: 'selection'**:自动添加复选框列
- **render 函数**:使用 Vue 的 `h()` 函数创建虚拟 DOM
- **ElTag 组件**:将状态值渲染为彩色标签
- **ElAvatar 组件**:将图片 URL 渲染为缩略图
## 5. 搜索表单 querySchemas.ts
定义搜索表单的字段配置,使用项目封装的 `FormSchema` 类型。
### 完整代码
```typescript
import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
field: 'name',
component: 'Input',
label: '案例名称',
componentProps: { placeholder: '请输入案例名称', clearable: true },
},
{
field: 'type',
component: 'Select',
label: '案例类型',
componentProps: {
placeholder: '请输入案例类型',
clearable: true,
options: [
{ label: '类型1', value: 1 },
{ label: '类型2', value: 2 },
{ label: '类型3', value: 3 },
{ label: '类型4', value: 4 },
],
},
},
{
field: 'status',
component: 'Select',
label: '案例状态',
componentProps: {
placeholder: '请输入案例状态',
clearable: true,
options: [
{ label: '正常', value: 1 },
{ label: '禁用', value: 2 },
],
},
},
];
```
### 关键点
- **field**:字段名,与后端查询参数对应
- **component**:表单组件类型:`Input` / `Select` / `DatePicker` 等
- **componentProps**:组件属性,透传给 ElementPlus 组件
- **options**:Select 组件的下拉选项
## 文件关系图
```
index.vue(主页面)
├── import schemas from './querySchemas' ← 搜索表单配置
├── import columns from './columns' ← 表格列配置
├── import editDialog from './edit.vue' ← 编辑弹窗组件
├── import detailDialog from './detail.vue' ← 详情弹窗组件
└── import API from '@/api/tool/example' ← 接口请求
edit.vue(编辑弹窗)
└── import API from '@/api/tool/example' ← 接口请求
detail.vue(详情弹窗)
└── import API from '@/api/tool/example' ← 接口请求
```
## 开发要点
1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下
2. **5 个文件各司其职**:主页面、编辑弹窗、详情弹窗、列定义、搜索 Schema
3. **弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面
4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、图片缩略图
5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段
6. **新增/编辑通过 `id` 区分**:0 为新增,非 0 为编辑
7. **防重复提交使用 `useLockFn`**:提交时按钮 loading
## 总结
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`detail.vue`(详情弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)五个文件。