优化:首页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
|
||||
|
||||
@ -26,8 +31,7 @@
|
||||
|
||||
## 事件
|
||||
|
||||
- `navigate`:点击卡片跳转
|
||||
- `trade`:点击 Yes/No 发起交易,携带 market 信息
|
||||
- `openTrade(side, market?)`:点击 Yes/No 发起交易,携带 market 信息(single 时为当前 market;multi 时为当前 outcome)
|
||||
|
||||
## 使用方式
|
||||
|
||||
@ -47,8 +51,7 @@
|
||||
:is-new="item.isNew"
|
||||
:market-id="item.marketId"
|
||||
:clob-token-ids="item.clobTokenIds"
|
||||
@navigate="goToDetail"
|
||||
@trade="openTrade"
|
||||
@openTrade="openTrade"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
## 核心能力
|
||||
|
||||
- 顶部导航栏:返回、PolyMarket 标题、Login 或余额+用户名+头像菜单
|
||||
- 多语言入口:右侧地球图标(`mdi-earth`)+ 当前语言文案,点击打开语言选择菜单
|
||||
- 登录态:`userStore.isLoggedIn` 控制展示
|
||||
- 用户名:`nickName` 或 `userName` 显示在头像左侧(有值时)
|
||||
- 挂载时与 `isLoggedIn` 变为 true 时:拉取用户信息与余额(`router.isReady()` + `nextTick` 后执行),确保钱包登录、刷新页面后头像和用户名正确显示
|
||||
|
||||
@ -4,15 +4,14 @@
|
||||
|
||||
## 功能用途
|
||||
|
||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。底部 Footer 已抽成独立组件 `Footer.vue`。
|
||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
|
||||
|
||||
## 核心能力
|
||||
|
||||
- **分类导航**:三层级分类选择(一级 Tab、二级文字标签、三级 Tab)
|
||||
- **分类导航**:三层级分类选择(一级 `v-tabs`、二级 `v-chip`、三级 `v-tabs`)
|
||||
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
||||
- **搜索**:可按关键词搜索事件
|
||||
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选;切换语言时重新请求分类接口并刷新列表
|
||||
- **Footer**:使用 `<Footer />` 组件,包含品牌、链接、语言选择、免责声明
|
||||
|
||||
## 数据流
|
||||
|
||||
@ -36,26 +35,27 @@ activeTagIds = [1351, 1368] // 合并所有选中层级的 tagIds
|
||||
const activeTagIds = computed(() => {
|
||||
const activeIds = layerActiveValues.value
|
||||
const tagIdSet = new Set<number>()
|
||||
|
||||
|
||||
// 遍历每一层选中的节点,收集所有 tagIds(含父级)
|
||||
let currentNodes = filterVisible(categoryTree.value)
|
||||
for (let i = 0; i < activeIds.length; i++) {
|
||||
const selectedId = activeIds[i]
|
||||
if (!selectedId) continue
|
||||
|
||||
|
||||
const node = currentNodes.find((n) => n.id === selectedId)
|
||||
if (node?.tagIds && node.tagIds.length > 0) {
|
||||
node.tagIds.forEach((id) => tagIdSet.add(id))
|
||||
}
|
||||
|
||||
|
||||
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×字体高度)
|
||||
|
||||
137
src/App.vue
137
src/App.vue
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user