签入版本
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 9.12 新增页面开发步骤
|
||||
|
||||
:::tip 概述
|
||||
以 example 模块为例,演示从 API 到视图的完整开发步骤。一个完整模块涉及 6 个文件:API 接口、表格列配置、查询表单配置、列表页、编辑弹窗、详情弹窗。
|
||||
以 example 模块为例,演示从 API 到视图的完整开发步骤(代码来自项目实际的 `tool/example` 模块)。一个完整模块涉及 6 个文件:API 接口、表格列配置、查询表单配置、列表页、编辑弹窗、详情弹窗。
|
||||
:::
|
||||
|
||||
## 开发流程
|
||||
@@ -90,64 +90,60 @@ export function exampleBatchDelete(data: any) {
|
||||
|
||||
## 步骤二:创建表格列配置
|
||||
|
||||
表格列使用 `title` / `key` 字段声明表头与数据字段,`render` 函数参数为当前行对象:
|
||||
|
||||
```typescript
|
||||
// ui/src/views/tool/example/columns.ts
|
||||
// ui/src/views/tool/example/columns.ts(来自项目实际代码)
|
||||
import { h } from 'vue';
|
||||
import { ElAvatar, ElTag } from 'element-plus';
|
||||
import { NTag, NAvatar } from 'naive-ui';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
type: 'selection', // 多选框列
|
||||
title: '案例名称',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
// 普通文本渲染(默认按 key 取值)
|
||||
},
|
||||
{
|
||||
label: 'ID',
|
||||
prop: 'id',
|
||||
fixed: 'left',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
label: '名称',
|
||||
prop: 'name',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'statusText',
|
||||
minWidth: 100,
|
||||
// 选项标签渲染:按值映射文字与颜色
|
||||
render(record) {
|
||||
const statusMap = {
|
||||
0: { text: '禁用', type: 'danger' },
|
||||
1: { text: '启用', type: 'success' },
|
||||
};
|
||||
const status = statusMap[record.row.status];
|
||||
return h(ElTag, { type: status?.type || 'info' }, {
|
||||
default: () => status?.text || record.row.statusText,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '头像',
|
||||
prop: 'avatar',
|
||||
minWidth: 80,
|
||||
title: '案例图片',
|
||||
key: 'avatar',
|
||||
width: 120,
|
||||
// 图片渲染:以方形头像形式展示
|
||||
render(record) {
|
||||
return h(ElAvatar, {
|
||||
return h(NAvatar, {
|
||||
size: 48,
|
||||
src: record.row.avatar,
|
||||
shape: 'square',
|
||||
fit: 'fill',
|
||||
src: record.avatar,
|
||||
round: false,
|
||||
objectFit: 'fill',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUser',
|
||||
minWidth: 100,
|
||||
title: '案例状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
// 选项标签渲染:按选项值映射文字与标签颜色
|
||||
render(record) {
|
||||
const statusMap = {
|
||||
1: { text: '在用', type: 'success' },
|
||||
2: { text: '停用', type: 'error' },
|
||||
};
|
||||
const status = statusMap[record.status];
|
||||
return h(
|
||||
NTag,
|
||||
{ bordered: false, type: status?.type || 'info' },
|
||||
{ default: () => status?.text || record.status },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
title: '创建人',
|
||||
key: 'createUser',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
@@ -158,39 +154,42 @@ export const columns = [
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `type` | string | 列类型:`selection`(多选框)/ `index`(序号) |
|
||||
| `label` | string | 表头标题 |
|
||||
| `prop` | string | 数据字段名(camelCase) |
|
||||
| `title` | string | 表头标题 |
|
||||
| `key` | string | 数据字段名(camelCase) |
|
||||
| `fixed` | string | 固定列:`left` / `right` |
|
||||
| `width` | number | 固定宽度 |
|
||||
| `minWidth` | number | 最小宽度(自适应) |
|
||||
| `render` | function | 自定义渲染函数 |
|
||||
| `render` | function | 自定义渲染函数,参数为当前行对象 |
|
||||
| `auth` | string[] | 按权限控制列显示(可选) |
|
||||
|
||||
## 步骤三:创建查询表单配置
|
||||
|
||||
`component` 取值为对应的组件名(`NInput`/`NSelect`/`NInputNumber`/`NDatePicker` 等):
|
||||
|
||||
```typescript
|
||||
// ui/src/views/tool/example/querySchemas.ts
|
||||
// ui/src/views/tool/example/querySchemas.ts(来自项目实际代码)
|
||||
import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
label: '名称',
|
||||
component: 'NInput',
|
||||
label: '案例名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入名称',
|
||||
placeholder: '请输入案例名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
component: 'Select',
|
||||
label: '状态',
|
||||
component: 'NSelect',
|
||||
label: '案例状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
placeholder: '请选择案例状态',
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
{ label: '在用', value: 1 },
|
||||
{ label: '停用', value: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -201,293 +200,347 @@ export const schemas: FormSchema[] = [
|
||||
|
||||
| component | 适用场景 | 说明 |
|
||||
|-----------|---------|------|
|
||||
| `Input` | 字符串字段 | 文本输入框 |
|
||||
| `Select` | 枚举/选项字段 | 下拉选择框 |
|
||||
| `InputNumber` | 数字字段 | 数字输入框 |
|
||||
| `DatePicker` | 日期字段 | 日期选择器 |
|
||||
| `NInput` | 字符串字段 | 文本输入框 |
|
||||
| `NSelect` | 枚举/选项字段 | 下拉选择框(选项通过 `componentProps.options` 传入) |
|
||||
| `NInputNumber` | 数字字段 | 数字输入框 |
|
||||
| `NDatePicker` | 日期字段 | 日期选择器 |
|
||||
| `NRadioGroup` | 少量选项(≤3 个) | 单选组 |
|
||||
|
||||
## 步骤四:创建列表页
|
||||
|
||||
列表页使用 `BasicForm`(搜索)+ `BasicTable`(表格,内置分页与操作列)组合:
|
||||
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/index.vue -->
|
||||
<!-- ui/src/views/tool/example/index.vue(节选,来自项目实际代码) -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- 查询表单 -->
|
||||
<SearchForm :schemas="querySchemas" @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row :gutter="10" style="margin-bottom: 10px;">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<n-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</n-card>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange" v-loading="loading">
|
||||
<el-table-column v-for="col in columns" v-bind="col" :key="col.prop || col.type">
|
||||
<template v-if="col.render" #default="scope">
|
||||
<component :is="() => col.render(scope)" />
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
ref="basicTableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@update:checked-row-keys="onCheckedRow"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
|
||||
新增
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
:disabled="!rowKeys.length"
|
||||
@click="handleDelete()"
|
||||
v-perm="['sys:example:batchDelete']"
|
||||
>
|
||||
批量删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pageNo"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@change="fetchData"
|
||||
style="margin-top: 10px; justify-content: flex-end;"
|
||||
/>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog ref="editRef" @success="fetchData" />
|
||||
<EditDialog
|
||||
ref="editRef"
|
||||
v-if="editVisible"
|
||||
v-model:visible="editVisible"
|
||||
:id="currentId"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<DetailDialog v-model:visible="detailVisible" :id="detailId" />
|
||||
<DetailDialog v-model:visible="detailVisible" :id="currentId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, nextTick, reactive, ref } from 'vue';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
import { columns } from './columns';
|
||||
import { schemas as querySchemas } from './querySchemas';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { schemas } from './querySchemas';
|
||||
import EditDialog from './edit.vue';
|
||||
import DetailDialog from './detail.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const basicTableRef = ref();
|
||||
const editRef = ref();
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const pageNo = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const selectedIds = ref<number[]>([]);
|
||||
const editVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const detailId = ref(0);
|
||||
const currentId = ref(0);
|
||||
const rowKeys = ref([]);
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getExamplePage({
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
// 查询参数(与搜索表单字段一一对应)
|
||||
const formParams = reactive({ name: '', status: '' });
|
||||
|
||||
// 操作列(render 返回 TableAction,auth 控制按钮权限)
|
||||
const actionColumn = reactive({
|
||||
width: 280,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction as any, {
|
||||
actions: [
|
||||
{ label: '编辑', type: 'info', onClick: handleEdit.bind(null, record), auth: ['sys:example:update'] },
|
||||
{ label: '详情', type: 'info', onClick: handleDetail.bind(null, record), auth: ['sys:example:detail'] },
|
||||
{ label: '删除', type: 'error', onClick: handleDelete.bind(null, record), auth: ['sys:example:delete'] },
|
||||
],
|
||||
});
|
||||
tableData.value = res.records;
|
||||
total.value = res.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// 搜索
|
||||
const handleSearch = (params) => {
|
||||
pageNo.value = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
pageNo.value = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
editRef.value.open();
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
editRef.value.open(row);
|
||||
};
|
||||
|
||||
// 详情
|
||||
const handleDetail = (row) => {
|
||||
detailId.value = row.id;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id) => {
|
||||
await ElMessageBox.confirm('确认删除该记录?', '提示', { type: 'warning' });
|
||||
await exampleDelete(id);
|
||||
ElMessage.success('删除成功');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录?`, '提示', { type: 'warning' });
|
||||
await exampleBatchDelete({ ids: selectedIds.value });
|
||||
ElMessage.success('删除成功');
|
||||
fetchData();
|
||||
// 分页数据请求(BasicTable 内置分页,直接返回接口结果)
|
||||
const loadDataTable = async (params) => {
|
||||
rowKeys.value = [];
|
||||
return await getExamplePage({ ...formParams, ...params });
|
||||
};
|
||||
|
||||
// 多选变化
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedIds.value = rows.map(row => row.id);
|
||||
};
|
||||
function onCheckedRow(keys) {
|
||||
rowKeys.value = keys;
|
||||
}
|
||||
|
||||
onMounted(() => fetchData());
|
||||
// 刷新表格(不传参回第一页)
|
||||
function reloadTable(noRefresh = '') {
|
||||
basicTableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||
}
|
||||
|
||||
// 注册搜索表单
|
||||
const [register] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 80,
|
||||
schemas,
|
||||
});
|
||||
|
||||
// 搜索 / 重置
|
||||
function handleSubmit(values) {
|
||||
for (const key in formParams) formParams[key] = '';
|
||||
Object.assign(formParams, values);
|
||||
reloadTable();
|
||||
}
|
||||
function handleReset() {
|
||||
for (const key in formParams) formParams[key] = '';
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
// 新增 / 编辑 / 详情
|
||||
const handleAdd = async () => {
|
||||
currentId.value = 0;
|
||||
editVisible.value = true;
|
||||
await nextTick();
|
||||
editRef.value.openModal();
|
||||
};
|
||||
async function handleEdit(record) {
|
||||
currentId.value = record.id;
|
||||
editVisible.value = true;
|
||||
await nextTick();
|
||||
editRef.value.openModal();
|
||||
}
|
||||
function handleDetail(record) {
|
||||
currentId.value = record.id;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
// 删除(单条 / 批量共用,先弹确认框)
|
||||
function handleDelete(record?) {
|
||||
const ids = record ? [record.id] : rowKeys.value;
|
||||
if (!ids.length) {
|
||||
message.warning('请选择要删除的数据');
|
||||
return;
|
||||
}
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: record ? '确定删除该记录?' : `确定删除选中的 ${ids.length} 条记录?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
record ? await exampleDelete(record.id) : await exampleBatchDelete({ ids });
|
||||
message.success('删除成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 步骤五:创建编辑弹窗
|
||||
|
||||
使用项目封装的 `basicModal`(`useModal`)+ `n-form`:
|
||||
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/edit.vue -->
|
||||
<!-- ui/src/views/tool/example/edit.vue(节选,来自项目实际代码) -->
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="formData.id ? '编辑' : '新增'" width="500px">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择类型">
|
||||
<el-option v-for="item in typeOptions" :key="item.value" :label="item.name" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
|
||||
<basicModal
|
||||
@register="modalRegister"
|
||||
ref="modalRef"
|
||||
@on-ok="handleSubmit"
|
||||
@on-close="dialogClose"
|
||||
>
|
||||
<template #default>
|
||||
<n-form class="ls-form" ref="formRef" :model="formData" label-placement="left" label-width="85px">
|
||||
<n-form-item
|
||||
label="案例名称"
|
||||
path="name"
|
||||
:rule="{ required: true, message: '请输入案例名称', trigger: 'blur' }"
|
||||
>
|
||||
<n-input v-model:value="formData.name" placeholder="请输入案例名称" clearable />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="案例类型" path="type">
|
||||
<n-select
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择案例类型"
|
||||
clearable
|
||||
filterable
|
||||
:options="typeOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="案例状态" path="status">
|
||||
<n-radio-group v-model:value="formData.status">
|
||||
<n-radio :value="1">在用</n-radio>
|
||||
<n-radio :value="2">停用</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="案例排序" path="sort">
|
||||
<n-input-number v-model:value="formData.sort" :min="0" :max="99999" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basicModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { exampleAdd, exampleUpdate, getExampleDetail } from '@/api/tool/example';
|
||||
import { getDictItemList } from '@/api/common/index';
|
||||
import { ElMessage } from 'element-plus';
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
import { getExampleDetail, exampleAdd, exampleUpdate } from '@/api/tool/example';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitting = ref(false);
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
const message = useMessage();
|
||||
const formRef = ref();
|
||||
const typeOptions = ref([]);
|
||||
|
||||
const formData = ref({
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, required: true, default: false },
|
||||
id: { type: Number, required: true, default: 0 }, // 0 表示新增
|
||||
});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '类型1', value: 1 },
|
||||
{ label: '类型2', value: 2 },
|
||||
{ label: '类型3', value: 3 },
|
||||
{ label: '类型4', value: 4 },
|
||||
];
|
||||
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
type: '',
|
||||
type: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||
};
|
||||
// 弹窗配置
|
||||
const [modalRegister, { openModal, setSubLoading }] = useModal({
|
||||
title: props.id ? '编辑案例演示' : '新增案例演示',
|
||||
subBtuText: '确定',
|
||||
width: 600,
|
||||
});
|
||||
|
||||
// 打开弹窗
|
||||
const open = async (row?) => {
|
||||
visible.value = true;
|
||||
formData.value = { id: 0, name: '', type: '', status: 1, sort: 0 };
|
||||
|
||||
// 加载字典选项
|
||||
typeOptions.value = await getDictItemList('example_type');
|
||||
|
||||
// 编辑时加载详情
|
||||
if (row?.id) {
|
||||
const detail = await getExampleDetail(row.id);
|
||||
Object.assign(formData.value, detail);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value.validate();
|
||||
submitting.value = true;
|
||||
// 加载详情(编辑时回填)
|
||||
const setFormData = async () => {
|
||||
try {
|
||||
if (formData.value.id) {
|
||||
await exampleUpdate(formData.value);
|
||||
ElMessage.success('修改成功');
|
||||
} else {
|
||||
await exampleAdd(formData.value);
|
||||
ElMessage.success('新增成功');
|
||||
const data = await getExampleDetail(props.id);
|
||||
for (const key in formData) {
|
||||
if (data[key] != null) formData[key] = data[key];
|
||||
}
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
} catch (error) {
|
||||
message.error('获取数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
// 提交:校验 → 新增/修改 → 提示 → 关闭并刷新
|
||||
const handleSubmit = useLockFn(async () => {
|
||||
await formRef.value?.validate();
|
||||
props.id ? await exampleUpdate(formData) : await exampleAdd(formData);
|
||||
message.success('操作成功');
|
||||
setSubLoading(false);
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
});
|
||||
|
||||
const dialogClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
defineExpose({ openModal, setFormData });
|
||||
</script>
|
||||
```
|
||||
|
||||
## 步骤六:创建详情弹窗
|
||||
|
||||
详情弹窗使用 `n-modal`(preset="card")+ `n-descriptions`:
|
||||
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/detail.vue -->
|
||||
<!-- ui/src/views/tool/example/detail.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="名称:">
|
||||
<n-modal
|
||||
v-model:show="props.visible"
|
||||
preset="card"
|
||||
title="案例演示详情"
|
||||
:style="{ width: '750px' }"
|
||||
:close-on-click-modal="false"
|
||||
@close="dialogClose"
|
||||
>
|
||||
<n-descriptions :column="2" bordered label-placement="left">
|
||||
<n-descriptions-item label="案例名称:">
|
||||
{{ formData.name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型:">
|
||||
{{ formData.typeText || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态:">
|
||||
<el-tag :type="formData.status === 1 ? 'success' : 'danger'">
|
||||
{{ formData.statusText || '-' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序:">
|
||||
{{ formData.sort ?? '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="头像:">
|
||||
<el-image
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="案例图片:">
|
||||
<n-image
|
||||
v-if="formData.avatar"
|
||||
:src="formData.avatar"
|
||||
:preview-src-list="[formData.avatar]"
|
||||
fit="cover"
|
||||
style="width: 80px; height: 80px"
|
||||
:preview-src="formData.avatar"
|
||||
width="80"
|
||||
height="80"
|
||||
object-fit="cover"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人:">
|
||||
{{ formData.createUser || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间:">
|
||||
{{ formData.createTime || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="案例状态:">
|
||||
<n-tag :type="formData.status === 1 ? 'success' : 'error'" bordered>
|
||||
{{ formData.status === 1 ? '在用' : '停用' }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="案例排序:">
|
||||
{{ formData.sort || '-' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="emit('update:visible', false)">关闭</el-button>
|
||||
<div style="text-align: right">
|
||||
<n-button @click="dialogClose">关闭</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
import { NImage } from 'naive-ui';
|
||||
import { getExampleDetail } from '@/api/tool/example';
|
||||
|
||||
const emit = defineEmits(['update:visible']);
|
||||
@@ -498,15 +551,12 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
type: '',
|
||||
typeText: '',
|
||||
status: 0,
|
||||
statusText: '',
|
||||
sort: 0,
|
||||
avatar: '',
|
||||
createUser: '',
|
||||
createTime: '',
|
||||
type: 0,
|
||||
status: 0,
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
// 加载详情
|
||||
@@ -514,21 +564,25 @@ const loadDetail = async () => {
|
||||
try {
|
||||
const data = await getExampleDetail(props.id);
|
||||
for (const key in formData) {
|
||||
if (data[key] != null) {
|
||||
formData[key] = data[key];
|
||||
}
|
||||
if (data[key] != null) formData[key] = data[key];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听弹窗显示
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal && props.id) {
|
||||
loadDetail();
|
||||
}
|
||||
}, { immediate: true });
|
||||
const dialogClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
// 监听弹窗显示,加载数据
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.id) loadDetail();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user