优化:首页UI风格调整

This commit is contained in:
ivan 2026-03-18 20:08:12 +08:00
parent 9d92b4cbfe
commit 170e788116
6 changed files with 261 additions and 283 deletions

View File

@ -4,7 +4,12 @@
## 功能用途
首页事件卡片组件展示市场标题、概率、Yes/No 或 Up/Down 按钮、多选项轮播。支持单一single与多选项multi两种展示类型点击可跳转交易详情或直接发起交易。
首页事件卡片组件展示市场标题、概率、Yes/No 或 Up/Down 按钮。支持单一single与多选项multi两种展示类型
- single展示一组 Yes/No或 Up/Down按钮
- multi在固定高度区域内以**上下滚动列表**展示多个 outcome每行右侧提供 Yes/No 快捷下单按钮
点击卡片本体会跳转详情页;点击按钮会触发下单事件(不会触发卡片跳转)。
## Props
@ -26,8 +31,7 @@
## 事件
- `navigate`:点击卡片跳转
- `trade`:点击 Yes/No 发起交易,携带 market 信息
- `openTrade(side, market?)`:点击 Yes/No 发起交易,携带 market 信息single 时为当前 marketmulti 时为当前 outcome
## 使用方式
@ -47,8 +51,7 @@
:is-new="item.isNew"
:market-id="item.marketId"
:clob-token-ids="item.clobTokenIds"
@navigate="goToDetail"
@trade="openTrade"
@openTrade="openTrade"
/>
```

View File

@ -9,6 +9,7 @@
## 核心能力
- 顶部导航栏返回、PolyMarket 标题、Login 或余额+用户名+头像菜单
- 多语言入口:右侧地球图标(`mdi-earth`+ 当前语言文案,点击打开语言选择菜单
- 登录态:`userStore.isLoggedIn` 控制展示
- 用户名:`nickName``userName` 显示在头像左侧(有值时)
- 挂载时与 `isLoggedIn` 变为 true 时:拉取用户信息与余额(`router.isReady()` + `nextTick` 后执行),确保钱包登录、刷新页面后头像和用户名正确显示

View File

@ -4,15 +4,14 @@
## 功能用途
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。底部 Footer 已抽成独立组件 `Footer.vue`
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
## 核心能力
- **分类导航**:三层级分类选择(一级 Tab、二级文字标签、三级 Tab
- **分类导航**:三层级分类选择(一级 `v-tabs`、二级 `v-chip`、三级 `v-tabs`
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
- **搜索**:可按关键词搜索事件
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表
- **Footer**:使用 `<Footer />` 组件,包含品牌、链接、语言选择、免责声明
## 数据流
@ -51,11 +50,12 @@ const activeTagIds = computed(() => {
currentNodes = filterVisible(node?.children)
}
return Array.from(tagIdSet) // 去重后的数组
return Array.from(tagIdSet) // 去重后的数组
})
```
**示例**
- 选中「政治」tagIds: [1351])→ activeTagIds = [1351]
- 选中「政治 → 特朗普」tagIds: [1351] + [1368])→ activeTagIds = [1351, 1368]
@ -68,3 +68,17 @@ const activeTagIds = computed(() => {
1. **新增分类层级**:修改 `MAX_LAYER` 常量,调整模板渲染逻辑
2. **自定义筛选逻辑**:修改 `activeTagIds` 计算属性
3. **列表缓存策略**:调整 `getEventListCache` / `setEventListCache`
### 二级分类 Chip 样式
- **未选中**`variant="outlined"`(白底灰边黑字,接近 Vuetify 默认 Chip 外观)
- **选中**`variant="tonal" + color="primary"`(浅蓝底强调,文字为主题主色)
### 一级/三级 Tabs 样式Home 页定制)
- **无下划线**:隐藏 Vuetify tabs 的 slider/indicator
- **选中加粗**:选中 tab 使用更粗字重
- **无点击水波纹**:禁用 ripple避免点击涟漪效果
- **无 hover 效果**:禁用鼠标悬停时的背景/遮罩变化
- **更紧凑**:取消 `v-tab` 默认 `min-width` 限制,避免强制占宽
- **第三层更紧凑**:三级 tabs 的高度收敛到约 28px接近 2×字体高度

View File

@ -13,6 +13,12 @@ const router = useRouter()
const userStore = useUserStore()
const currentRoute = computed(() => route.path)
const currentLocaleLabel = computed(() => {
return (
localeStore.localeOptions.find((o) => o.value === localeStore.currentLocale)?.label ??
String(localeStore.currentLocale)
)
})
async function refreshUserData() {
if (!userStore.isLoggedIn) return
@ -36,72 +42,79 @@ watch(
<template>
<v-app>
<v-app-bar color="primary" dark>
<v-app-bar color="surface" elevation="0">
<div class="app-bar-inner">
<v-btn
v-if="currentRoute !== '/'"
icon
variant="text"
class="back-btn"
:aria-label="t('common.back')"
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-app-bar-title v-if="currentRoute === '/'">PolyMarket</v-app-bar-title>
<v-spacer></v-spacer>
<v-menu location="bottom" :close-on-content-click="true" class="locale-menu">
<template #activator="{ props }">
<v-btn v-bind="props" icon variant="text" class="locale-btn" :aria-label="t('common.more')">
<v-icon size="20">mdi-translate</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="opt in localeStore.localeOptions"
:key="opt.value"
:title="opt.label"
:active="localeStore.currentLocale === opt.value"
@click="localeStore.setLocale(opt.value)"
/>
</v-list>
</v-menu>
<v-btn
v-if="!userStore.isLoggedIn"
text
to="/login"
:class="{ active: currentRoute === '/login' }"
>
{{ t('common.login') }}
</v-btn>
<template v-else>
<v-btn
class="balance-btn"
v-if="currentRoute !== '/'"
icon
variant="text"
min-width="auto"
padding="4 12"
@click="$router.push('/wallet')"
class="back-btn"
:aria-label="t('common.back')"
@click="$router.back()"
>
<span class="balance-text">${{ userStore.balance }}</span>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-menu location="bottom" :close-on-content-click="false">
<v-app-bar-title v-if="currentRoute === '/'">PolyMarket</v-app-bar-title>
<v-spacer></v-spacer>
<v-menu location="bottom" :close-on-content-click="true" class="locale-menu">
<template #activator="{ props }">
<v-btn v-bind="props" icon variant="text" class="avatar-btn">
<v-avatar size="36" color="primary">
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" />
<v-icon v-else>mdi-account</v-icon>
</v-avatar>
<v-btn
v-bind="props"
variant="text"
size="small"
class="locale-btn"
:aria-label="`${t('common.more')} (${currentLocaleLabel})`"
>
<v-icon size="20">mdi-earth</v-icon>
<span class="locale-label">{{ currentLocaleLabel }}</span>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
:title="userStore.user?.nickName || userStore.user?.userName || t('common.user')"
disabled
v-for="opt in localeStore.localeOptions"
:key="opt.value"
:title="opt.label"
:active="localeStore.currentLocale === opt.value"
@click="localeStore.setLocale(opt.value)"
/>
<v-list-item :title="t('common.logout')" @click="userStore.logout()" />
</v-list>
</v-menu>
</template>
<v-btn
v-if="!userStore.isLoggedIn"
text
to="/login"
:class="{ active: currentRoute === '/login' }"
>
{{ t('common.login') }}
</v-btn>
<template v-else>
<v-btn
class="balance-btn"
variant="text"
min-width="auto"
padding="4 12"
@click="$router.push('/wallet')"
>
<span class="balance-text">${{ userStore.balance }}</span>
</v-btn>
<v-menu location="bottom" :close-on-content-click="false">
<template #activator="{ props }">
<v-btn v-bind="props" icon variant="text" class="avatar-btn">
<v-avatar size="36" color="primary">
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" />
<v-icon v-else>mdi-account</v-icon>
</v-avatar>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
:title="userStore.user?.nickName || userStore.user?.userName || t('common.user')"
disabled
/>
<v-list-item :title="t('common.logout')" @click="userStore.logout()" />
</v-list>
</v-menu>
</template>
</div>
</v-app-bar>
<v-main>
@ -140,7 +153,7 @@ watch(
}
.balance-btn {
color: rgba(255, 255, 255, 0.9);
color: rgba(0, 0, 0, 0.87);
text-transform: none;
}
@ -150,6 +163,20 @@ watch(
}
.back-btn {
color: rgba(255, 255, 255, 0.9);
color: rgba(0, 0, 0, 0.87);
}
.locale-btn {
text-transform: none;
}
.locale-label {
margin-left: 6px;
font-size: 12px;
color: rgba(0, 0, 0, 0.72);
max-width: 88px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -1,5 +1,11 @@
<template>
<v-card class="market-card" elevation="0" :rounded="'lg'" @click="navigateToDetail">
<v-card
class="market-card"
elevation="0"
:rounded="'lg'"
:ripple="false"
@click="navigateToDetail"
>
<div class="market-card-content">
<!-- Top Section头像 + 标题 + 半圆概率 / 多选项时仅标题 -->
<div class="top-section">
@ -70,80 +76,38 @@
<!-- 多选项类型左右滑动轮播每页一项 = 左侧 title+% 右侧 Yes/No -->
<div v-else class="multi-section">
<v-carousel
v-model="currentSlide"
class="outcome-carousel"
:continuous="true"
:show-arrows="false"
hide-delimiters
height="35"
>
<v-carousel-item
v-for="(outcome, idx) in props.outcomes"
:key="idx"
class="outcome-slide"
>
<div class="outcome-slide-inner">
<div class="outcome-row">
<div class="outcome-item">
<span class="outcome-title">{{ outcome.title }}</span>
<span class="outcome-chance-value">{{ outcome.chanceValue }}%</span>
</div>
<div class="outcome-buttons">
<v-btn
class="option-yes option-yes-no-compact"
:color="'#b8e0b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@click.stop="openTradeMulti('yes', outcome)"
>
<span class="option-text-yes">{{ outcome.yesLabel ?? 'Yes' }}</span>
</v-btn>
<v-btn
class="option-no option-yes-no-compact"
:color="'#f0b8b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@click.stop="openTradeMulti('no', outcome)"
>
<span class="option-text-no">{{ outcome.noLabel ?? 'No' }}</span>
</v-btn>
</div>
<!-- 改为上下滚动固定高度手指上下滑动切换不同 outcome隐藏左右控制条 -->
<div class="outcome-vertical" @mousedown.stop @touchstart.stop @click.stop>
<div v-for="(outcome, idx) in props.outcomes" :key="idx" class="outcome-vertical-item">
<div class="outcome-row">
<div class="outcome-item">
<span class="outcome-title">{{ outcome.title }}</span>
<span class="outcome-chance-value">{{ outcome.chanceValue }}%</span>
</div>
<div class="outcome-buttons">
<v-btn
class="option-yes option-yes-no-compact"
:color="'#b8e0b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@click.stop="openTradeMulti('yes', outcome)"
>
<span class="option-text-yes">{{ outcome.yesLabel ?? 'Yes' }}</span>
</v-btn>
<v-btn
class="option-no option-yes-no-compact"
:color="'#f0b8b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@click.stop="openTradeMulti('no', outcome)"
>
<span class="option-text-no">{{ outcome.noLabel ?? 'No' }}</span>
</v-btn>
</div>
</div>
</v-carousel-item>
</v-carousel>
<div class="carousel-controls" @mousedown.stop @touchstart.stop>
<button
type="button"
class="carousel-arrow carousel-arrow--left"
:disabled="outcomeCount <= 1"
aria-label="上一项"
@click.stop="prevSlide"
>
<v-icon size="18">mdi-chevron-left</v-icon>
</button>
<div class="carousel-dots">
<button
v-for="(_, idx) in props.outcomes ?? []"
:key="idx"
type="button"
:class="['carousel-dot', { active: currentSlide === idx }]"
:aria-label="`选项 ${idx + 1}`"
@click.stop="currentSlide = idx"
/>
</div>
<button
type="button"
class="carousel-arrow carousel-arrow--right"
:disabled="outcomeCount <= 1"
aria-label="下一项"
@click.stop="nextSlide"
>
<v-icon size="18">mdi-chevron-right</v-icon>
</button>
</div>
</div>
@ -159,7 +123,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import type { EventCardOutcome } from '../api/event'
@ -233,18 +197,6 @@ const props = withDefaults(
)
const isMulti = computed(() => props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1)
const currentSlide = ref(0)
const outcomeCount = computed(() => (props.outcomes ?? []).length)
function prevSlide() {
if (outcomeCount.value <= 1) return
currentSlide.value = (currentSlide.value - 1 + outcomeCount.value) % outcomeCount.value
}
function nextSlide() {
if (outcomeCount.value <= 1) return
currentSlide.value = (currentSlide.value + 1) % outcomeCount.value
}
// stroke-dashoffset π*26 0° 180°
const SEMI_CIRCLE_LENGTH = Math.PI * 26
@ -335,17 +287,28 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
.market-card {
position: relative;
width: 310px;
border: 1px solid #f0f0f0;
height: 176px;
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e7e7e7;
border: none;
padding: 12px;
cursor: pointer;
transition: all 0.2s ease;
transition: box-shadow 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.market-card:hover {
border-color: #d0d0d0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
/* 去掉 v-card--link 的点击遮罩与水波纹 */
.market-card:deep(.v-card__overlay),
.market-card:deep(.v-ripple__container) {
display: none;
}
.market-card:deep(.v-card::before) {
background: none;
}
.market-card-content {
@ -456,11 +419,12 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
margin-top: 2px;
}
/* Options Section单 market 时靠底对齐 */
/* Options Section单 market 时靠底对齐,向下留 16px 间距 */
.options-section {
display: flex;
gap: 12px;
margin-top: auto;
padding-top: 32px;
}
.option-yes {
@ -515,31 +479,27 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
flex: 1 1 auto;
min-height: 0;
margin-top: auto;
padding-bottom: 14px;
padding-bottom: 0;
box-sizing: border-box;
overflow: hidden;
}
.outcome-carousel {
/* 与 .multi-section 同高:占满父级剩余空间,内部可上下滚动 */
.outcome-vertical {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
scroll-snap-type: y mandatory;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
touch-action: pan-y;
}
.outcome-carousel :deep(.v-carousel__container) {
touch-action: pan-y pinch-zoom;
}
.outcome-slide {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
box-sizing: border-box;
}
.outcome-slide-inner {
width: 100%;
.outcome-vertical-item {
scroll-snap-align: start;
min-height: 40px;
display: flex;
align-items: center;
box-sizing: border-box;
@ -598,72 +558,6 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
font-size: 13px;
}
.carousel-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0;
margin-bottom: 10px;
flex-shrink: 0;
}
.carousel-arrow {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 6px;
background-color: #f0f0f0;
color: #333;
cursor: pointer;
transition:
background-color 0.2s,
color 0.2s;
box-shadow: none;
}
.carousel-arrow:hover:not(:disabled) {
background-color: #e0e0e0;
color: #000;
}
.carousel-arrow:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.carousel-dots {
display: flex;
justify-content: center;
gap: 6px;
padding: 0;
flex-shrink: 0;
}
.carousel-dot {
width: 6px;
height: 6px;
border-radius: 50%;
border: none;
background-color: #e0e0e0;
cursor: pointer;
transition: background-color 0.2s ease;
padding: 0;
}
.carousel-dot:hover {
background-color: #bdbdbd;
}
.carousel-dot.active {
background-color: rgb(var(--v-theme-primary));
width: 8px;
height: 6px;
border-radius: 3px;
}
.new-badge {
display: inline-block;
margin-right: 6px;

View File

@ -8,7 +8,7 @@
class="home-tab-bar home-tab-bar--inline"
@update:model-value="onCategorySelect(0, $event)"
>
<v-tab v-for="item in categoryLayers[0]" :key="item.id" :value="item.id">
<v-tab v-for="item in categoryLayers[0]" :key="item.id" :value="item.id" :ripple="false">
{{ item.label }}
</v-tab>
</v-tabs>
@ -108,16 +108,17 @@
<div v-if="categoryLayers.length >= 2" class="home-category-layers-23-scroll">
<div class="home-category-layer home-category-layer--icon">
<div class="home-category-icon-row">
<button
<v-chip
v-for="item in categoryLayers[1]"
:key="item.id"
type="button"
class="home-category-icon-item"
:class="{ 'home-category-icon-item--active': layerActiveValues[1] === item.id }"
:color="layerActiveValues[1] === item.id ? 'primary' : undefined"
:variant="layerActiveValues[1] === item.id ? 'tonal' : 'outlined'"
size="small"
@click="onCategorySelect(1, item.id)"
>
<span class="home-category-icon-label">{{ item.label }}</span>
</button>
</v-chip>
</div>
</div>
<div
@ -129,7 +130,12 @@
class="home-tab-bar home-tab-bar--compact"
@update:model-value="onCategorySelect(2, $event)"
>
<v-tab v-for="item in categoryLayers[2]" :key="item.id" :value="item.id">
<v-tab
v-for="item in categoryLayers[2]"
:key="item.id"
:value="item.id"
:ripple="false"
>
{{ item.label }}
</v-tab>
</v-tabs>
@ -215,16 +221,22 @@
/>
</v-bottom-sheet>
</v-container>
<Footer />
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'Home' })
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed, watch } from 'vue'
import {
ref,
onMounted,
onUnmounted,
onActivated,
onDeactivated,
nextTick,
computed,
watch,
} from 'vue'
import { useDisplay } from 'vuetify'
import Footer from '../components/Footer.vue'
import MarketCard from '../components/MarketCard.vue'
import TradeComponent from '../components/TradeComponent.vue'
import {
@ -528,17 +540,13 @@ const homeTradeMarketPayload = computed(() => {
if (!m) return undefined
const marketId = m.marketId ?? m.id
const yesPrice =
m.yesPrice != null && Number.isFinite(m.yesPrice)
? Math.min(1, Math.max(0, m.yesPrice))
: 0.5
m.yesPrice != null && Number.isFinite(m.yesPrice) ? Math.min(1, Math.max(0, m.yesPrice)) : 0.5
const noPrice =
m.noPrice != null && Number.isFinite(m.noPrice)
? Math.min(1, Math.max(0, m.noPrice))
: 1 - yesPrice
const outcomes =
m.yesLabel != null || m.noLabel != null
? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No']
: undefined
m.yesLabel != null || m.noLabel != null ? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No'] : undefined
return { marketId, yesPrice, noPrice, title: m.title, clobTokenIds: m.clobTokenIds, outcomes }
})
@ -702,6 +710,7 @@ onActivated(() => {
max-width: 2560px;
margin-left: auto;
margin-right: auto;
padding-top: 0 !important;
}
.home-header {
@ -821,7 +830,7 @@ onActivated(() => {
top: 64px;
z-index: 10;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); */
}
.home-category-layer1-row {
@ -1007,30 +1016,11 @@ onActivated(() => {
.home-category-icon-item {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
padding: 4px 10px;
border: none;
border-radius: 6px;
background: transparent;
color: #64748b;
font-size: 12px;
cursor: pointer;
transition:
background-color 0.2s,
color 0.2s;
}
.home-category-icon-item:hover {
background-color: rgba(0, 0, 0, 0.04);
color: #334155;
}
.home-category-icon-item--active {
background-color: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
/* 未选中outlined 描边改为灰色(不改文字颜色) */
.home-category-icon-item.v-chip--variant-outlined {
border-color: rgba(0, 0, 0, 0.28) !important;
}
.home-category-icon {
@ -1049,7 +1039,8 @@ onActivated(() => {
}
.home-category-layer--third {
padding: 12px 16px 0;
/* 让第三层高度更紧凑:接近 2 倍字体高度 */
padding: 2px 16px;
}
.home-category-action-btn {
@ -1067,9 +1058,57 @@ onActivated(() => {
box-shadow: none;
}
.home-tab-bar :deep(.v-tab__slider),
.home-tab-bar :deep(.v-tabs-slider) {
display: none !important;
}
/* 覆写 Vuetify density 默认的 tabs 高度变量(在 v-tabs 根元素上) */
:deep(.v-tabs--density-default.home-tab-bar) {
--v-tabs-height: unset !important;
}
.home-tab-bar :deep(.v-tab--selected) {
font-weight: 700;
}
.home-tab-bar :deep(.v-ripple__container) {
display: none !important;
}
/* 去掉鼠标悬停hover视觉效果禁用按钮 overlay/伪元素 */
.home-tab-bar :deep(.v-btn__overlay),
.home-tab-bar :deep(.v-btn__underlay) {
display: none !important;
}
.home-tab-bar :deep(.v-tab:hover) {
background-color: transparent !important;
}
.home-tab-bar :deep(.v-tab) {
min-width: unset !important;
}
.home-tab-bar--compact :deep(.v-tab) {
min-height: 40px;
min-height: 28px;
height: 28px;
line-height: 28px;
font-size: 14px;
padding-left: 10px;
padding-right: 10px;
}
/* 第三层 tabs收敛容器高度避免底部多余留白 */
.home-tab-bar--compact :deep(.v-tabs) {
min-height: 28px !important;
height: 28px !important;
}
.home-tab-bar--compact :deep(.v-slide-group__container) {
min-height: 28px !important;
height: 28px !important;
}
.home-tab-bar--compact :deep(.v-slide-group__content) {
align-items: center;
}
.home-card {
@ -1103,7 +1142,7 @@ onActivated(() => {
}
}
/* 页面布局flex 列Footer 通过 margin-top: auto 贴底 */
/* 页面布局flex 列 */
.home-page {
display: flex;
flex-direction: column;