优化:首页UI风格调整
This commit is contained in:
parent
9d92b4cbfe
commit
170e788116
@ -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 时为当前 market;multi 时为当前 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"
|
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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` 后执行),确保钱包登录、刷新页面后头像和用户名正确显示
|
||||||
|
|||||||
@ -4,15 +4,14 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。底部 Footer 已抽成独立组件 `Footer.vue`。
|
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
|
||||||
|
|
||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
- **分类导航**:三层级分类选择(一级 Tab、二级文字标签、三级 Tab)
|
- **分类导航**:三层级分类选择(一级 `v-tabs`、二级 `v-chip`、三级 `v-tabs`)
|
||||||
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
||||||
- **搜索**:可按关键词搜索事件
|
- **搜索**:可按关键词搜索事件
|
||||||
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表
|
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表
|
||||||
- **Footer**:使用 `<Footer />` 组件,包含品牌、链接、语言选择、免责声明
|
|
||||||
|
|
||||||
## 数据流
|
## 数据流
|
||||||
|
|
||||||
@ -56,6 +55,7 @@ const activeTagIds = computed(() => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**示例**:
|
**示例**:
|
||||||
|
|
||||||
- 选中「政治」(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×字体高度)
|
||||||
|
|||||||
37
src/App.vue
37
src/App.vue
@ -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,7 +42,7 @@ 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-btn
|
||||||
v-if="currentRoute !== '/'"
|
v-if="currentRoute !== '/'"
|
||||||
@ -52,8 +58,15 @@ watch(
|
|||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-menu location="bottom" :close-on-content-click="true" class="locale-menu">
|
<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="locale-btn" :aria-label="t('common.more')">
|
<v-btn
|
||||||
<v-icon size="20">mdi-translate</v-icon>
|
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>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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,20 +76,9 @@
|
|||||||
|
|
||||||
<!-- 多选项类型:左右滑动轮播,每页一项 = 左侧 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"
|
|
||||||
: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-row">
|
||||||
<div class="outcome-item">
|
<div class="outcome-item">
|
||||||
<span class="outcome-title">{{ outcome.title }}</span>
|
<span class="outcome-title">{{ outcome.title }}</span>
|
||||||
@ -113,37 +108,6 @@
|
|||||||
</div>
|
</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>
|
|
||||||
<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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user