会员频道页面结构
会员频道页面复用了学习页面的 Layout,整体分为上下两部分:
- 上半部分:采用 Flex 布局,左右各占 50%,用于展示频道基本信息
- 下半部分:会员权益表格,这是本节的核心开发内容
会员权益表格的核心需求是动态渲染:
| 动态维度 | 说明 |
|---|---|
| 列(用户等级) | 普通用户、会员、高级会员,可扩展至钻石、铂金等 |
| 行(权益项) | 课程答疑、学分兑换、优质专栏等,可动态增减 |
| 单元格内容 | 布尔值(是/否)、字符串(折扣信息)、数字等混合类型 |
数据结构设计
实现动态渲染的关键在于合理设计数据结构。核心思路是:以拥有最多权限的用户为基准渲染所有行,其他用户的权限是其子集。
// 权益项类型
interface UserRightsItem {
name: string // 权益名称,如 "课程答疑"
path?: string // 权益路径(可选)
value: string | boolean | number // 权益值:布尔、字符串或数字
}
// 用户等级类型
interface UserRights {
name: string // 等级名称:普通用户、会员、高级会员
rights: UserRightsItem[] // 该等级拥有的权益列表
price: number // 对应价格
}
// 组件 Props 类型
interface VipTypes {
items: UserRights[]
}
typescript
Mock 数据示例
const defaultItems: UserRights[] = [
{
name: '普通用户',
price: 0,
rights: [
{ name: '课程答疑', value: true },
{ name: '学分兑换', value: true },
],
},
{
name: '会员',
price: 199,
rights: [
{ name: '课程答疑', value: true },
{ name: '学分兑换', value: true },
{ name: '优质专栏', value: true },
],
},
{
name: '高级会员',
price: 399,
rights: [
{ name: '课程答疑', value: true },
{ name: '学分兑换', value: true },
{ name: '优质专栏', value: true },
{ name: '精品微课', value: '8折' },
],
},
]
typescript
权益合并与去重
不同用户等级可能拥有不同的权益项,需要将所有用户的权益合并为一个完整的列表,用于渲染表格的行。
const allRights = computed(() => {
// 1. 从所有用户中提取权益名称,得到嵌套数组
// 2. 使用 flat(Infinity) 扁平化
// 3. 使用 Set 去重
return Array.from(
new Set(
props.items.flatMap(item =>
item.rights.map(r => r.name)
)
)
)
})
typescript
关键技术点:
Array.flatMap()-- 等价于map()+flat()的组合,一步完成映射和扁平化flat(Infinity)-- 处理任意深度的嵌套数组new Set()-- 自动去重
获取单元格值
根据权益名称和用户等级,查找对应的值:
function getValue(item: string, user: UserRights): string | boolean | number | undefined {
return user.rights.find(r => r.name === item)?.value
}
typescript
如果该用户等级不拥有某项权益,find() 返回 undefined,此时渲染为不可用图标。
模板渲染逻辑
表格结构分为 <thead> 和 <tbody> 两部分:
<template>
<div class="w-full p-4">
<h2 class="text-center text-xl font-bold mb-4">会员权益</h2>
<table class="w-full text-center">
<!-- 表头:用户等级 -->
<thead>
<tr>
<td />
<td v-for="item in props.items" :key="item.name">
<div class="flex flex-col items-center">
<span class="text-xl">{{ item.name }}</span>
<span class="text-sm text-gray-200">
{{ item.price === 0 ? '免费' : `¥${item.price}/年` }}
</span>
</div>
</td>
</tr>
</thead>
<!-- 表体:权益项 -->
<tbody>
<tr
v-for="(item, index) in allRights"
:key="index"
:class="{ 'bg-sky-50': index % 2 === 0 }"
>
<td class="text-gray-400">{{ item }}</td>
<td v-for="user in props.items" :key="user.name">
<!-- 无权限 -->
<Icon
v-if="typeof getValue(item, user) === 'undefined'"
icon="mdi:close-circle"
class="text-gray-400"
:width="24"
/>
<!-- 有权限(布尔为 true) -->
<Icon
v-else-if="typeof getValue(item, user) === 'boolean'
&& getValue(item, user) === true"
icon="mdi:check-circle"
class="text-sky-400"
:width="24"
/>
<!-- 其他值(折扣等) -->
<span v-else class="text-orange-500 font-bold">
{{ getValue(item, user) }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</template>
vue
样式要点
使用 UnoCSS 原子类快速完成样式调整:
| 样式需求 | UnoCSS 类名 |
|---|---|
| 表格宽度撑满 | w-full |
| 偶数行背景色 | :class="{ 'bg-sky-50': index % 2 === 0 }" |
| 单元格内边距 | p-2 |
| 第一列圆角 | rounded-l (配合 first-child) |
| 最后一列圆角 | rounded-r (配合 last-child) |
| 勾选图标蓝色 | text-sky-400 |
| 关闭图标灰色 | text-gray-400 |
| 折扣文字橙色 | text-orange-500 |
在 <style lang="scss"> 中使用 SCSS 设置表格细粒度样式:
table {
td {
@apply p-2;
&:first-child {
@apply w-20 rounded-l;
}
&:last-child {
@apply rounded-r;
}
}
}
scss
组件封装
组件完成后,可将其从页面中提取为独立组件:
- 创建
components/VipRightsTable.vue - 通过
defineProps<VipTypes>()接收数据 - 在页面中通过
<VipRightsTable :items="vipData" />引用
这种表格结构也可以使用 <ul>/<li> 配合 Grid 布局来替代 <table>,在某些场景下更加灵活。
要点总结
- 数据结构设计:以"最完整权限的用户"为基准,其他用户权限是其子集
- 权益合并:使用
flatMap+Set实现多用户权益的合并去重 - 动态渲染:表格的行列数量和内容完全由数据驱动
- 类型安全:使用 TypeScript interface 约束 props 类型
- 样式框架:结合 UnoCSS 原子类和 SCSS 完成快速样式开发
↑