优化:首页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 ## Props
@ -26,8 +31,7 @@
## 事件 ## 事件
- `navigate`:点击卡片跳转 - `openTrade(side, market?)`:点击 Yes/No 发起交易,携带 market 信息single 时为当前 marketmulti 时为当前 outcome
- `trade`:点击 Yes/No 发起交易,携带 market 信息
## 使用方式 ## 使用方式
@ -47,8 +51,7 @@
:is-new="item.isNew" :is-new="item.isNew"
:market-id="item.marketId" :market-id="item.marketId"
:clob-token-ids="item.clobTokenIds" :clob-token-ids="item.clobTokenIds"
@navigate="goToDetail" @openTrade="openTrade"
@trade="openTrade"
/> />
``` ```

View File

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

View File

@ -4,15 +4,14 @@
## 功能用途 ## 功能用途
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。底部 Footer 已抽成独立组件 `Footer.vue` 首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
## 核心能力 ## 核心能力
- **分类导航**:三层级分类选择(一级 Tab、二级文字标签、三级 Tab - **分类导航**:三层级分类选择(一级 `v-tabs`、二级 `v-chip`、三级 `v-tabs`
- **事件列表**:卡片式展示,支持下拉刷新、触底加载 - **事件列表**:卡片式展示,支持下拉刷新、触底加载
- **搜索**:可按关键词搜索事件 - **搜索**:可按关键词搜索事件
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表 - **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表
- **Footer**:使用 `<Footer />` 组件,包含品牌、链接、语言选择、免责声明
## 数据流 ## 数据流
@ -36,26 +35,27 @@ activeTagIds = [1351, 1368] // 合并所有选中层级的 tagIds
const activeTagIds = computed(() => { const activeTagIds = computed(() => {
const activeIds = layerActiveValues.value const activeIds = layerActiveValues.value
const tagIdSet = new Set<number>() const tagIdSet = new Set<number>()
// 遍历每一层选中的节点,收集所有 tagIds含父级 // 遍历每一层选中的节点,收集所有 tagIds含父级
let currentNodes = filterVisible(categoryTree.value) let currentNodes = filterVisible(categoryTree.value)
for (let i = 0; i < activeIds.length; i++) { for (let i = 0; i < activeIds.length; i++) {
const selectedId = activeIds[i] const selectedId = activeIds[i]
if (!selectedId) continue if (!selectedId) continue
const node = currentNodes.find((n) => n.id === selectedId) const node = currentNodes.find((n) => n.id === selectedId)
if (node?.tagIds && node.tagIds.length > 0) { if (node?.tagIds && node.tagIds.length > 0) {
node.tagIds.forEach((id) => tagIdSet.add(id)) node.tagIds.forEach((id) => tagIdSet.add(id))
} }
currentNodes = filterVisible(node?.children) currentNodes = filterVisible(node?.children)
} }
return Array.from(tagIdSet) // 去重后的数组 return Array.from(tagIdSet) // 去重后的数组
}) })
``` ```
**示例** **示例**
- 选中「政治」tagIds: [1351])→ activeTagIds = [1351] - 选中「政治」tagIds: [1351])→ activeTagIds = [1351]
- 选中「政治 → 特朗普」tagIds: [1351] + [1368])→ activeTagIds = [1351, 1368] - 选中「政治 → 特朗普」tagIds: [1351] + [1368])→ activeTagIds = [1351, 1368]
@ -68,3 +68,17 @@ const activeTagIds = computed(() => {
1. **新增分类层级**:修改 `MAX_LAYER` 常量,调整模板渲染逻辑 1. **新增分类层级**:修改 `MAX_LAYER` 常量,调整模板渲染逻辑
2. **自定义筛选逻辑**:修改 `activeTagIds` 计算属性 2. **自定义筛选逻辑**:修改 `activeTagIds` 计算属性
3. **列表缓存策略**:调整 `getEventListCache` / `setEventListCache` 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 userStore = useUserStore()
const currentRoute = computed(() => route.path) const currentRoute = computed(() => route.path)
const currentLocaleLabel = computed(() => {
return (
localeStore.localeOptions.find((o) => o.value === localeStore.currentLocale)?.label ??
String(localeStore.currentLocale)
)
})
async function refreshUserData() { async function refreshUserData() {
if (!userStore.isLoggedIn) return if (!userStore.isLoggedIn) return
@ -36,72 +42,79 @@ watch(
<template> <template>
<v-app> <v-app>
<v-app-bar color="primary" dark> <v-app-bar color="surface" elevation="0">
<div class="app-bar-inner"> <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 <v-btn
class="balance-btn" v-if="currentRoute !== '/'"
icon
variant="text" variant="text"
min-width="auto" class="back-btn"
padding="4 12" :aria-label="t('common.back')"
@click="$router.push('/wallet')" @click="$router.back()"
> >
<span class="balance-text">${{ userStore.balance }}</span> <v-icon>mdi-arrow-left</v-icon>
</v-btn> </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 }"> <template #activator="{ props }">
<v-btn v-bind="props" icon variant="text" class="avatar-btn"> <v-btn
<v-avatar size="36" color="primary"> v-bind="props"
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" /> variant="text"
<v-icon v-else>mdi-account</v-icon> size="small"
</v-avatar> 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> </v-btn>
</template> </template>
<v-list density="compact"> <v-list density="compact">
<v-list-item <v-list-item
:title="userStore.user?.nickName || userStore.user?.userName || t('common.user')" v-for="opt in localeStore.localeOptions"
disabled :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-list>
</v-menu> </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> </div>
</v-app-bar> </v-app-bar>
<v-main> <v-main>
@ -140,7 +153,7 @@ watch(
} }
.balance-btn { .balance-btn {
color: rgba(255, 255, 255, 0.9); color: rgba(0, 0, 0, 0.87);
text-transform: none; text-transform: none;
} }
@ -150,6 +163,20 @@ watch(
} }
.back-btn { .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> </style>

View File

@ -1,5 +1,11 @@
<template> <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"> <div class="market-card-content">
<!-- Top Section头像 + 标题 + 半圆概率 / 多选项时仅标题 --> <!-- Top Section头像 + 标题 + 半圆概率 / 多选项时仅标题 -->
<div class="top-section"> <div class="top-section">
@ -70,80 +76,38 @@
<!-- 多选项类型左右滑动轮播每页一项 = 左侧 title+% 右侧 Yes/No --> <!-- 多选项类型左右滑动轮播每页一项 = 左侧 title+% 右侧 Yes/No -->
<div v-else class="multi-section"> <div v-else class="multi-section">
<v-carousel <!-- 改为上下滚动固定高度手指上下滑动切换不同 outcome隐藏左右控制条 -->
v-model="currentSlide" <div class="outcome-vertical" @mousedown.stop @touchstart.stop @click.stop>
class="outcome-carousel" <div v-for="(outcome, idx) in props.outcomes" :key="idx" class="outcome-vertical-item">
:continuous="true" <div class="outcome-row">
:show-arrows="false" <div class="outcome-item">
hide-delimiters <span class="outcome-title">{{ outcome.title }}</span>
height="35" <span class="outcome-chance-value">{{ outcome.chanceValue }}%</span>
> </div>
<v-carousel-item <div class="outcome-buttons">
v-for="(outcome, idx) in props.outcomes" <v-btn
:key="idx" class="option-yes option-yes-no-compact"
class="outcome-slide" :color="'#b8e0b8'"
> :rounded="'sm'"
<div class="outcome-slide-inner"> :text="true"
<div class="outcome-row"> elevation="0"
<div class="outcome-item"> @click.stop="openTradeMulti('yes', outcome)"
<span class="outcome-title">{{ outcome.title }}</span> >
<span class="outcome-chance-value">{{ outcome.chanceValue }}%</span> <span class="option-text-yes">{{ outcome.yesLabel ?? 'Yes' }}</span>
</div> </v-btn>
<div class="outcome-buttons"> <v-btn
<v-btn class="option-no option-yes-no-compact"
class="option-yes option-yes-no-compact" :color="'#f0b8b8'"
:color="'#b8e0b8'" :rounded="'sm'"
:rounded="'sm'" :text="true"
:text="true" elevation="0"
elevation="0" @click.stop="openTradeMulti('no', outcome)"
@click.stop="openTradeMulti('yes', outcome)" >
> <span class="option-text-no">{{ outcome.noLabel ?? 'No' }}</span>
<span class="option-text-yes">{{ outcome.yesLabel ?? 'Yes' }}</span> </v-btn>
</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> </div>
</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> </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>
</div> </div>
@ -159,7 +123,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { EventCardOutcome } from '../api/event' 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 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° // stroke-dashoffset π*26 0° 180°
const SEMI_CIRCLE_LENGTH = Math.PI * 26 const SEMI_CIRCLE_LENGTH = Math.PI * 26
@ -335,17 +287,28 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
.market-card { .market-card {
position: relative; position: relative;
width: 310px; width: 310px;
border: 1px solid #f0f0f0;
height: 176px; height: 176px;
background-color: #ffffff; background-color: #ffffff;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e7e7e7; border: none;
padding: 12px; padding: 12px;
cursor: pointer; 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 { .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 { .market-card-content {
@ -456,11 +419,12 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
margin-top: 2px; margin-top: 2px;
} }
/* Options Section单 market 时靠底对齐 */ /* Options Section单 market 时靠底对齐,向下留 16px 间距 */
.options-section { .options-section {
display: flex; display: flex;
gap: 12px; gap: 12px;
margin-top: auto; margin-top: auto;
padding-top: 32px;
} }
.option-yes { .option-yes {
@ -515,31 +479,27 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
margin-top: auto; margin-top: auto;
padding-bottom: 14px; padding-bottom: 0;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
} }
.outcome-carousel { /* 与 .multi-section 同高:占满父级剩余空间,内部可上下滚动 */
.outcome-vertical {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
border-radius: 8px; border-radius: 8px;
overflow: hidden; scroll-snap-type: y mandatory;
flex-shrink: 0; -webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
touch-action: pan-y;
} }
.outcome-carousel :deep(.v-carousel__container) { .outcome-vertical-item {
touch-action: pan-y pinch-zoom; scroll-snap-align: start;
} min-height: 40px;
.outcome-slide {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
box-sizing: border-box;
}
.outcome-slide-inner {
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
@ -598,72 +558,6 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
font-size: 13px; 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 { .new-badge {
display: inline-block; display: inline-block;
margin-right: 6px; margin-right: 6px;

View File

@ -8,7 +8,7 @@
class="home-tab-bar home-tab-bar--inline" class="home-tab-bar home-tab-bar--inline"
@update:model-value="onCategorySelect(0, $event)" @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 }} {{ item.label }}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -108,16 +108,17 @@
<div v-if="categoryLayers.length >= 2" class="home-category-layers-23-scroll"> <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-layer home-category-layer--icon">
<div class="home-category-icon-row"> <div class="home-category-icon-row">
<button <v-chip
v-for="item in categoryLayers[1]" v-for="item in categoryLayers[1]"
:key="item.id" :key="item.id"
type="button"
class="home-category-icon-item" 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)" @click="onCategorySelect(1, item.id)"
> >
<span class="home-category-icon-label">{{ item.label }}</span> <span class="home-category-icon-label">{{ item.label }}</span>
</button> </v-chip>
</div> </div>
</div> </div>
<div <div
@ -129,7 +130,12 @@
class="home-tab-bar home-tab-bar--compact" class="home-tab-bar home-tab-bar--compact"
@update:model-value="onCategorySelect(2, $event)" @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 }} {{ item.label }}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -215,16 +221,22 @@
/> />
</v-bottom-sheet> </v-bottom-sheet>
</v-container> </v-container>
<Footer />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'Home' }) 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 { useDisplay } from 'vuetify'
import Footer from '../components/Footer.vue'
import MarketCard from '../components/MarketCard.vue' import MarketCard from '../components/MarketCard.vue'
import TradeComponent from '../components/TradeComponent.vue' import TradeComponent from '../components/TradeComponent.vue'
import { import {
@ -528,17 +540,13 @@ const homeTradeMarketPayload = computed(() => {
if (!m) return undefined if (!m) return undefined
const marketId = m.marketId ?? m.id const marketId = m.marketId ?? m.id
const yesPrice = const yesPrice =
m.yesPrice != null && Number.isFinite(m.yesPrice) m.yesPrice != null && Number.isFinite(m.yesPrice) ? Math.min(1, Math.max(0, m.yesPrice)) : 0.5
? Math.min(1, Math.max(0, m.yesPrice))
: 0.5
const noPrice = const noPrice =
m.noPrice != null && Number.isFinite(m.noPrice) m.noPrice != null && Number.isFinite(m.noPrice)
? Math.min(1, Math.max(0, m.noPrice)) ? Math.min(1, Math.max(0, m.noPrice))
: 1 - yesPrice : 1 - yesPrice
const outcomes = const outcomes =
m.yesLabel != null || m.noLabel != null m.yesLabel != null || m.noLabel != null ? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No'] : undefined
? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No']
: undefined
return { marketId, yesPrice, noPrice, title: m.title, clobTokenIds: m.clobTokenIds, outcomes } return { marketId, yesPrice, noPrice, title: m.title, clobTokenIds: m.clobTokenIds, outcomes }
}) })
@ -702,6 +710,7 @@ onActivated(() => {
max-width: 2560px; max-width: 2560px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 0 !important;
} }
.home-header { .home-header {
@ -821,7 +830,7 @@ onActivated(() => {
top: 64px; top: 64px;
z-index: 10; z-index: 10;
background-color: white; 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 { .home-category-layer1-row {
@ -1007,30 +1016,11 @@ onActivated(() => {
.home-category-icon-item { .home-category-icon-item {
flex-shrink: 0; 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 { /* 未选中outlined 描边改为灰色(不改文字颜色) */
background-color: rgba(0, 0, 0, 0.04); .home-category-icon-item.v-chip--variant-outlined {
color: #334155; border-color: rgba(0, 0, 0, 0.28) !important;
}
.home-category-icon-item--active {
background-color: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
} }
.home-category-icon { .home-category-icon {
@ -1049,7 +1039,8 @@ onActivated(() => {
} }
.home-category-layer--third { .home-category-layer--third {
padding: 12px 16px 0; /* 让第三层高度更紧凑:接近 2 倍字体高度 */
padding: 2px 16px;
} }
.home-category-action-btn { .home-category-action-btn {
@ -1067,9 +1058,57 @@ onActivated(() => {
box-shadow: none; 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) { .home-tab-bar--compact :deep(.v-tab) {
min-height: 40px; min-height: 28px;
height: 28px;
line-height: 28px;
font-size: 14px; 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 { .home-card {
@ -1103,7 +1142,7 @@ onActivated(() => {
} }
} }
/* 页面布局flex 列Footer 通过 margin-top: auto 贴底 */ /* 页面布局flex 列 */
.home-page { .home-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;