最近在做项目的时候,因为接触的比较多的基本上就是列表页和详情页,列表页主要就是类似于一个表格,展示所有数据,详情页主要就是指某条数据的详细信息,包括修改、新建一条数据、查看等都是对应详情页面,因此本次主要是想做一个关于列表页和详情页的实现的小分享,用到的技术栈是Vue3+Ts,使用到的组件库是ant design和tailwindcss。具体实现的页面可以看下图所示。
该页面是列表页,主要展示了用户的基本数据 ,勾选一条数据后可以点击查看,点击删除后可删除当前数据。
点击新建数据按钮后,进入到新建数据详情页,该页面是一个表单,用户输入完信息后提交校验通过后会在列表页新增一条数据。
在列表页选中一条数据后,点击查看,跳转到该条数据的详情页,详情页中展示该条数据的全部信息。
在详情页中点击修改按钮后可对当前数据进行修改,修改完成后点击提交,会改变当前数据的信息。
整体来说还是非常简单的,就是两个页面,一个列表页,一个详情页。下面来说一下具体的实现思路:
在实际项目中,所有的数据都是从后端获取的,因此所做的所有改变都会通过接口请求发送给后端,然后再次请求数据刷新页面。这里我是弄了一些假数据,在页面初始化的时候赋值,由于在列表页和详情页中切换时,我都是把所有数据进行路由传参,因此可以不用担心刷新页面而导致数据丢失的问题。
由于新建页面和查看详情页、编辑页都是对应同一个页面,因此考虑到组件的复用性,采用表单的结合v-if的形式进行控制。
const routes = [
{
path: "/",
component: () => import("../ant-table/Table.vue"),
},
{
path: "/details",
name: "details",
component: () => import("../view/Detail.vue"),
},
]
路由配置如图,默认进入列表页。
下面正式开始页面编写:
<template>
<div>
<router-view></router-view>
</div>
</template>
<script setup lang="ts"></script>
这里是App根组件,路由切换时用来呈现的内容。
interface DataType {
name: string;
age: string;
address: any;
sex: string;
school: string;
hobby: any;
birthday: any;
phone: string;
description: string;
position: {
isShow: boolean;
place: string;
};
}
定义数据类型,方便类型检测和书写规范。
<a-table
:row-selection="{
type: 'radio',
selectedRowKeys: rowSelection.selectedRowKeys,
onChange: onSelectChange,
}"
rowKey="name"
:columns="columns"
:data-source="data"
>
<template #bodyCell="{ column, record, text, index }">
<template v-if="column.dataIndex === 'operation'">
<span class="text-yn-400">
<delete-two-tone @click="deleteData(record, index)" />
</span>
</template>
<template v-if="column.dataIndex === 'index'">
<span>
{{ index }}
</span>
</template>
<template v-if="column.dataIndex === 'address'">
<span v-for="city in record.address">
{{ city }}
</span>
</template>
<template v-if="column.dataIndex === 'hobby'">
<span
v-for="hobby in record.hobby"
class="bg-green-200 rounded-lg py-1 px-1.5 mx-1.5"
>
{{ hobby }}
</span>
</template>
</template>
</a-table>
这部分的代码主要是定义的一些数据,包括表格的表头、初始化表格数据等。
// 跳转详情页
const toDetails = () => {
if (rowSelection.selectedRowKeys.length !== 1) {
message.error("请选择一条数据!");
} else {
let selectName = rowSelection.selectedRowKeys[0];
let usedata: any = toRaw(data.value);
router.push({
path: "/details",
query: { selectName, useData: JSON.stringify(usedata) },
});
}
};
跳转到查看详情页代码如图:这里的的query传参里面即传了当前选中数据的name(作为唯一标识符),也传了列表页的所有数据,注意路由传参这里,如果传的是非字符串类型的数据,需要用JSON.stringify包裹并在接收时用JSON.parse解析。
// 新建记录
const addNewData = () => {
let usedata: any = toRaw(data.value);
router.push({
path: "/details",
query: {
useData: JSON.stringify(usedata),
},
});
};
这里是点击新建数据时对应的代码。
这部分的代码就是初始化页面的操作和删除列表数据对应的代码,这里初始化数据我做了一个判断,就是页面加载时有没有数据,没有数据我就用一开始定义好的那几条数据,如果有数据(详情页传来的)就用详情页的数据。这里的妙处在于实际上只有页面一加载的时候是没有数据的,会使用默认数据,之后的每次页面路由切换,都会有数据了。
<a-form
:model="baseForm"
name="basic"
class="grid grid-cols-3"
ref="formRef"
:rules="baseInfoEules"
layout="vertical"
>
<!-- 姓名 -->
<div class="col-span-1 mx-3">
<a-form-item name="name" label="姓名">
<span v-if="!isEdit">{{ baseForm.name }}</span>
<a-input
:disabled="disableName"
v-else
v-model:value="baseForm.name"
/>
</a-form-item>
</div>
<!-- 年龄 -->
<div class="col-span-1 mx-3">
<a-form-item name="age" label="年龄">
<span v-if="!isEdit">{{ baseForm.age }}</span>
<a-input-number
class="w-full"
id="inputNumber"
v-else
v-model:value="baseForm.age"
:min="1"
:max="99"
/>
</a-form-item>
</div>
<!-- 性别 -->
<div class="col-span-1 mx-3">
<a-form-item name="sex" label="性别">
<span v-if="!isEdit">{{ baseForm.sex }}</span>
<a-select
v-else
v-model:value="baseForm.sex"
placeholder="Please select sex"
>
<a-select-option value="男">男</a-select-option>
<a-select-option value="女">女</a-select-option>
</a-select>
</a-form-item>
</div>
<!-- 地址 -->
<div class="col-span-1 mx-3">
<a-form-item name="address" label="地址">
<span v-if="!isEdit" v-for="city in baseForm.address">{{
city
}}</span>
<a-cascader
v-else
v-model:value="baseForm.address"
:options="cityOptions"
placeholder="Please select address"
/>
</a-form-item>
</div>
<!-- 学校 -->
<div class="col-span-1 mx-3">
<a-form-item name="school" label="学校">
<span v-if="!isEdit">{{ baseForm.school }}</span>
<a-select
v-else
v-model:value="baseForm.school"
placeholder="Please select a university"
>
<a-select-option value="西南大学">西南大学</a-select-option>
<a-select-option value="中南大学">中南大学</a-select-option>
<a-select-option value="武汉大学">武汉大学</a-select-option>
<a-select-option value="浙江大学">浙江大学</a-select-option>
</a-select>
</a-form-item>
</div>
<!-- 爱好 -->
<div class="col-span-1 mx-3">
<a-form-item name="hobby" label="爱好">
<span
v-if="!isEdit"
v-for="hobby in baseForm.hobby"
class="mr-1.5 bg-green-200 py-1 px-1.5 pb-1.5 rounded-lg"
>{{ hobby }}</span
>
<a-select
:options="options"
:fieldNames="{ label: 'label', value: 'value' }"
v-else
v-model:value="baseForm.hobby"
mode="multiple"
placeholder="Please select hobby"
>
</a-select>
</a-form-item>
</div>
<!-- 出生日期 -->
<div class="col-span-1 mx-3">
<a-form-item name="birthday" label="出生日期">
<span v-if="!isEdit">{{ baseForm.birthday }}</span>
<a-date-picker
placeholder="请选择时间"
style="width: 100%"
v-else
v-model:value="baseForm.birthday"
value-format="YYYY-MM-DD"
/>
</a-form-item>
</div>
<!-- 手机号 -->
<div class="col-span-1 mx-3">
<a-form-item name="phone" label="手机号码">
<span v-if="!isEdit">{{ baseForm.phone }}</span>
<a-input
v-else
v-model:value="baseForm.phone"
placeholder="请输入手机号码"
/>
</a-form-item>
</div>
<!-- 位置信息 -->
<a-form-item name="position" label="">
<div class="col-span-1 mx-3 pt-8">
<span class="mx-3">是否显示位置信息</span>
<a-switch v-model:checked="baseForm.position!.isShow" />
<span
class="mx-3 bg-blue-200 px-1.5 py-1 rounded-md"
v-if="baseForm.position!.isShow"
>{{ baseForm.position?.place }}
</span>
</div>
</a-form-item>
<!-- 说明 -->
<div class="col-span-3 mx-3">
<a-form-item name="description" label="情况说明">
<span v-if="!isEdit">{{ baseForm.description }}</span>
<a-textarea
v-else
:rows="4"
v-model:value="baseForm.description"
placeholder="请输入说明"
>
</a-textarea>
</a-form-item>
</div>
</a-form>
详情页的表单内容如下,通过v-if来判断当前是出于可编辑表单还是信息查看状态。
// 获取列表页数据
if (route.query.useData as any) {
useData.value = JSON.parse(route.query.useData as any) || [];
dataSource.value = useData.value;
}
获取列表页传递的数据
这部分的代码就不展示了,看图片上的注释应该也能知道我做的是一件什么事。
// 初始化数据
const initData = () => {
dataSource.value = useData.value;
// 表单置空
getEmptydata();
// 是否查看详情页
if (receiveName) {
dataSource.value.map((item) => {
if (item.name === receiveName) {
editData = item;
baseForm.value.name = item.name;
baseForm.value.age = item.age;
baseForm.value.sex = item.sex;
baseForm.value.school = item.school;
baseForm.value.address = item.address;
baseForm.value.hobby = item.hobby;
baseForm.value.phone = item.phone;
baseForm.value.birthday = item.birthday;
baseForm.value.description = item.description;
baseForm.value.position = item.position || {
isShow: false,
place: "湖北 武汉",
};
}
});
} else {
// 新增数据页面
isEdit.value = true;
disableName.value = false;
}
};
onMounted(() => {
initData();
});
这部分的代码就是比较关键的了,就是初始化详情页时,首先通过传参来判断是新建页还是查看页,在列表页中,如果点击查看详情是传了所选数据的name值的,而点击新建时没有传,这里就是通过判断是否接收到了路由传参的name值来判断是新建数据还是查看详情。
// 提交
const submit = () => {
// 修改后提交
if (receiveName) {
formRef.value?.validateFields().then(
(res) => {
isEdit.value = !isEdit.value;
let newData: any = res;
dataSource.value.map((item) => {
// 把修改的数据替换掉原来的数据
if (item.name == receiveName) {
item.name = newData?.name;
item.age = newData?.age;
item.sex = newData?.sex;
item.school = newData?.school;
item.address = newData?.address;
item.hobby = toRaw(newData?.hobby);
item.birthday = newData?.birthday;
item.phone = newData?.phone;
item.position = newData.position;
item.description = newData?.description;
}
});
router.push({
path: "/",
query: { obj: JSON.stringify(dataSource.value) },
});
},
(error) => {
console.log(error);
}
);
} else {
// 新增后提交
formRef.value?.validateFields().then(
(res) => {
console.log(res);
if (res) {
isEdit.value = !isEdit.value;
let newData: any = res;
dataSource.value.push(newData);
router.push({
path: "/",
query: { obj: JSON.stringify(dataSource.value) },
});
}
},
(error) => {
console.log(error);
}
);
}
};
这部分的代码也尤为重要,就是修改或者新增后,点击提交所触发的方法。
先判断是修改还是新建数据,然后走表单提交得到流程,如果是修改提交,那么将修改的数据替换掉原来的那条数据,如果是新增数据,那么直接push到数据里面。最后将修改完成后路由跳转到列表页,并将全部数据传参过去。
至此,一个简单的列表页和详情页便实现了。有些部分确实冗余度比较高,可以进一步优化改善,还有一些细节部分也可以优化,比如手机号的正则匹配等。
果然,还是调接口,直接从后端请求数据简单一些哈哈,不然每次都弄那么多数据,太麻烦了。
还有其他问题欢迎大家批评指正!
源码我后面会放到码云仓库里面