优化样式调整

This commit is contained in:
马丁 2026-05-23 12:30:02 +08:00
parent a21869d989
commit e4ecea471c
18 changed files with 375 additions and 397 deletions

View File

@ -10,6 +10,7 @@ import { storeToRefs } from 'pinia'
import { useSearchHistory } from './composables/useSearchHistory'
import type { LocaleCode } from './plugins/i18n'
import Toast from './components/Toast.vue'
import { getPmTagMain } from './api/category'
const route = useRoute()
const { t } = useI18n()
@ -19,7 +20,7 @@ const localeStore = useLocaleStore()
const menuStore = useMenuStore()
const localeMenuOpen = ref(false)
const { categoryLayers, layerActiveValues } = storeToRefs(menuStore)
const { categoryTree, categoryLayers, layerActiveValues } = storeToRefs(menuStore)
const searchHistory = useSearchHistory()
const searchHistoryList = computed(() => searchHistory.list.value)
const searchExpanded = ref(false)
@ -105,8 +106,15 @@ function selectHistoryItem(item: string) {
onSearchSubmit()
}
onMounted(() => {
refreshUserData()
onMounted(async () => {
await nextTick()
console.log('app onMounted')
if (layerActiveValues.value.length == 0) {
const res = await getPmTagMain()
if (res.code === 0 || res.code === 200) {
categoryTree.value = res.data
}
}
})
watch(
() => userStore.isLoggedIn,
@ -115,11 +123,19 @@ watch(
},
{ immediate: true },
)
async function onMenuClick(id: string) {
if (currentRoute.value != '/') {
router.push('/')
}
await nextTick()
menuStore.onCategorySelect(0, id)
}
</script>
<template>
<v-app>
<v-app-bar color="surface" elevation="0" height="112">
<v-app-bar color="surface" elevation="0" height="112" class="flex-row">
<div class="flex-column header-content">
<div class="app-bar-inner">
<v-btn v-if="currentRoute !== '/'" icon variant="text" class="back-btn" :aria-label="t('common.back')"
@ -138,7 +154,20 @@ watch(
</div>
</v-app-bar-title>
<v-spacer></v-spacer>
<div class="home-category-layer1-row line1">
<div class="app-tab-bar" height="48" @update:model-value="menuStore.onCategorySelect(0, $event)">
<div v-for="item in categoryLayers[0]" class="app-tab-bar-item"
:class="{ 'app-tab-bar-item-active': item.id === layerActiveValues[0] }" :key="item.id" :value="item.id"
@click="onMenuClick(item.id)">
{{ item.label }}
</div>
</div>
<v-btn icon variant="text" class="home-search-btn" :aria-label="t('common.search')" @click="expandSearch">
<v-icon size="24">mdi-magnify</v-icon>
</v-btn>
</div>
<v-spacer class="line2"></v-spacer>
<template v-if="!userStore.isLoggedIn">
<v-menu v-model="localeMenuOpen" :close-on-content-click="true" location="bottom"
@ -174,13 +203,12 @@ watch(
</div>
<!-- 提取的顶部菜单栏与搜索功能 -->
<div v-if="currentRoute === '/' && categoryLayers.length > 0" class="home-category-layer1-wrap">
<div class="home-category-layer1-row">
<div v-if="categoryLayers.length > 0" class="home-category-layer1-wrap">
<div class="home-category-layer1-row line2">
<div class="app-tab-bar" height="48" @update:model-value="menuStore.onCategorySelect(0, $event)">
<div v-for="item in categoryLayers[0]" class="app-tab-bar-item"
:class="{ 'app-tab-bar-item-active': item.id === layerActiveValues[0] }" :key="item.id"
:value="item.id"
@click="menuStore.onCategorySelect(0, item.id)">
:class="{ 'app-tab-bar-item-active': item.id === layerActiveValues[0] }" :key="item.id" :value="item.id"
@click="onMenuClick(item.id)">
{{ item.label }}
</div>
</div>
@ -263,10 +291,11 @@ watch(
<style scoped lang="scss">
.header-content {
width: 100%;
max-width: 1440px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
height: 112px;
margin: 0 auto;
flex: 1;
}
.app-bar-inner {
@ -382,10 +411,6 @@ watch(
}
@media (max-width: 600px) {
.app-bar-inner {
padding: 0 12px;
}
.brand-lockup {
gap: 10px;
}
@ -460,12 +485,25 @@ watch(
}
.home-category-layer1-row {
max-width: 1440px;
margin: 0 auto;
display: flex;
display: none;
align-items: center;
flex: 1;
}
.line1 {
@include gt1024 {
display: flex;
};
}
.line2 {
display: none;
@include lt1024 {
display: flex;
};
}
.app-tab-bar {
display: flex;
padding-left: 8px;
@ -582,10 +620,4 @@ watch(
cursor: pointer;
padding: 0;
}
@media (max-width: 600px) {
.home-category-layer1-wrap {
margin-left: 12px;
}
}
</style>

View File

@ -60,6 +60,12 @@
}
}
@mixin lt1024 {
@media screen and (max-width: 1025px) {
@content;
}
}
@mixin gt1024 {
@media screen and (min-width: 1024px) {
@content;

View File

@ -1,12 +1,12 @@
/* 全局:禁止 body 滚动,由 app-main-scroll 内部滚动,滚动条不覆盖底部导航 */
:global(html),
:global(body) {
background: rgb(252, 252, 252);
background: rgb(254, 254, 254);
height: 100%;
overflow: hidden;
}
:global(.v-application) {
background: rgb(252, 252, 252);
background: rgb(254, 254, 254);
height: 100vh;
overflow: hidden;
display: flex;
@ -35,4 +35,12 @@ button {
border: none;
background: none;
cursor: pointer;
}
.v-input--density-compact {
.v-field__input {
padding-inline: var(--v-field-padding-start) var(--v-field-padding-end);
padding-top: var(--v-field-input-padding-top);
padding-bottom: var(--v-field-input-padding-bottom);
}
}

View File

@ -21,4 +21,11 @@ $radius-md: 8px;
$radius-lg: 12px;
// 阴影
$shadow: 0 2px 12px 0 rgba(0,0,0,0.08);
$shadow: 0 2px 12px 0 rgba(0,0,0,0.08);
$yes-bg: #e3f7ea;
$no-bg: #fceded;
$yes-text: #3da763;
$no-text: #e23939;
$header-height: 112px;

View File

@ -12,9 +12,9 @@
<div class="footer-links">
<span class="footer-copyright">Alpha Market Inc. © 2026</span>
<span class="sep">·</span>
<a href="#">Privacy</a>
<a href="/doc/privacy">Privacy</a>
<span class="sep">·</span>
<a href="#">Terms of Use</a>
<a href="/doc/tos">Terms of Use</a>
<span class="sep">·</span>
<a href="#">Market Integrity</a>
<span class="sep">·</span>
@ -28,7 +28,7 @@
QCX LLC d/b/a Alpha Market US, a CFTC-regulated Designated Contract Market. This
international platform is not regulated by the CFTC and operates independently. Trading
involves substantial risk of loss. See our
<a href="#">Terms of Service</a> &amp; <a href="#">Privacy Policy</a>.
<a href="/doc/tos">Terms of Service</a> &amp; <a href="/doc/privacy">Privacy Policy</a>.
</p>
</div>
</footer>

View File

@ -53,7 +53,6 @@
<div class="options-section">
<v-btn
class="option-yes"
:color="'#b8e0b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@ -63,7 +62,6 @@
</v-btn>
<v-btn
class="option-no"
:color="'#f0b8b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@ -87,7 +85,6 @@
<div class="outcome-buttons">
<v-btn
class="option-yes option-yes-no-compact"
:color="'#b8e0b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@ -97,7 +94,6 @@
</v-btn>
<v-btn
class="option-no option-yes-no-compact"
:color="'#f0b8b8'"
:rounded="'sm'"
:text="true"
elevation="0"
@ -269,7 +265,7 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
}
</script>
<style scoped>
<style scoped lang="scss">
/* 单 market 与多 market 统一高度 */
.market-card {
position: relative;
@ -416,7 +412,7 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
.option-yes {
flex: 1;
background-color: #b8e0b8;
background-color: $yes-bg;
border-radius: 6px;
height: 40px;
}
@ -425,12 +421,12 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 500;
color: #006600;
color: $yes-text;
}
.option-no {
flex: 1;
background-color: #f0b8b8;
background-color: $no-bg;
border-radius: 6px;
height: 40px;
}
@ -439,7 +435,7 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
font-family: 'Inter', sans-serif;
font-size: 16px;
font-weight: 500;
color: #cc0000;
color: $no-text;
}
/* Bottom Section禁止被挤压避免与 multi-section 重叠 */

View File

@ -2216,7 +2216,7 @@ async function submitOrder() {
}
</script>
<style scoped>
<style scoped lang="scss">
/* 扁平化:移除所有阴影 */
.trade-component,
.trade-sheet-paper,
@ -2674,16 +2674,16 @@ async function submitOrder() {
.mobile-bar-yes {
flex: 1;
min-width: 0;
background: #16a34a;
color: #fff;
background: $yes-bg;
color: $yes-text;
padding: 14px 16px;
}
.mobile-bar-no {
flex: 1;
min-width: 0;
background: #dc2626;
color: #fff;
background: $no-bg;
color: $no-text;
padding: 14px 16px;
}

View File

@ -46,6 +46,7 @@ export const useMenuStore = defineStore('menu', () => {
/** 分类选中时:若有 children 则展开下一层并默认选中第一个,并重新加载列表 */
function onCategorySelect(layerIndex: number, selectedId: string) {
console.log('onCategorySelect', layerIndex, selectedId)
if (!selectedId || layerActiveValues.value[layerIndex] === selectedId) {
return
}

View File

@ -357,13 +357,16 @@ watch(
onUnmounted(removeLoadMoreObserver)
</script>
<style scoped>
<style scoped lang="scss">
.api-key-page {
min-height: 100vh;
background: #fcfcfc;
display: flex;
justify-content: center;
padding: 0;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.api-key-screen {

View File

@ -143,7 +143,7 @@ async function fetchLockInfo() {
}
</script>
<style scoped>
<style scoped lang="scss">
.earn-activity-page {
min-height: 100vh;
background: #f5f5f5;
@ -151,6 +151,9 @@ async function fetchLockInfo() {
justify-content: center;
align-items: flex-start;
padding: 16px;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.earn-activity-screen {

View File

@ -204,7 +204,6 @@
<TradeComponent
:market="tradeMarketPayload"
:initial-option="tradeInitialOption"
@submit="onTradeSubmit"
/>
</div>
</v-col>
@ -271,7 +270,6 @@
:market="tradeMarketPayload"
:initial-option="tradeInitialOption"
embedded-in-sheet
@submit="onTradeSubmit"
@order-success="onOrderSuccess"
/>
</v-bottom-sheet>
@ -429,13 +427,6 @@ const eventVolume = computed(() => {
return v != null ? formatVolume(v) : ''
})
const currentChance = computed(() => {
const m = selectedMarket.value
if (!m) return 0
return marketChance(m)
})
const selectedMarketVolume = computed(() => formatVolume(selectedMarket.value?.volume))
const marketExpiresAt = computed(() => {
const endDate = eventDetail.value?.endDate
return endDate ? formatExpiresAt(endDate) : ''
@ -477,7 +468,6 @@ const LINE_COLORS = [
'#ea580c',
'#4f46e5',
]
const MOBILE_BREAKPOINT = 600
/** 按市场依次请求 getPmPriceHistoryPublicmarket 传 clobTokenIds[0]YES token */
async function loadChartFromApi(): Promise<ChartSeriesItem[]> {
@ -595,10 +585,6 @@ const handleResize = () => {
chartInstance.resize(chartContainerRef.value.clientWidth, 320)
}
function selectMarket(index: number) {
selectedMarketIndex.value = index
}
/** 点击 Buy Yes/No选中该市场并把数据和方向传给购买组件移动端直接弹出交易弹窗 */
function openTrade(market: PmEventMarketItem, index: number, side: 'yes' | 'no') {
selectedMarketIndex.value = index
@ -628,18 +614,6 @@ function openSplitFromBar() {
tradeSheetOpen.value = true
}
function onTradeSubmit(payload: {
side: 'buy' | 'sell'
option: 'yes' | 'no'
limitPrice: number
shares: number
expirationEnabled: boolean
expirationTime: string
marketId?: string
}) {
// APIpayload marketId
}
const toastStore = useToastStore()
function onOrderSuccess() {
tradeSheetOpen.value = false
@ -823,10 +797,13 @@ watch(
)
</script>
<style scoped>
<style scoped lang="scss">
.event-markets-container {
padding: 24px;
min-height: 100vh;
max-width: 1440px;
margin: 0 auto;
margin-top: $header-height;
}
.event-markets-row {
@ -1194,13 +1171,13 @@ watch(
}
.buy-yes-btn {
background-color: #b8e0b8 !important;
color: #006600 !important;
background-color: $yes-bg;
color: $yes-text;
}
.buy-no-btn {
background-color: #f0b8b8 !important;
color: #cc0000 !important;
background-color: $no-bg;
color: $no-text;
}
/* 已结算子市场列表Pencil vb1xr */
@ -1327,16 +1304,16 @@ watch(
.mobile-bar-yes {
flex: 1;
min-width: 0;
background: #b8e0b8 !important;
color: #006600 !important;
background: $yes-bg;
color: $yes-text;
padding: 14px 16px;
}
.mobile-bar-no {
flex: 1;
min-width: 0;
background: #f0b8b8 !important;
color: #cc0000 !important;
background: $no-bg;
color: $no-text;
padding: 14px 16px;
}

View File

@ -22,41 +22,53 @@
</v-tabs>
</div>
</div>
<!-- 可滚动容器作为 v-pull-to-refresh 的父元素组件据此判断 scrollTop 仅在顶部时才响应下拉 -->
<div ref="scrollRef" class="home-list-scroll">
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
<div class="pull-to-refresh-inner">
<div ref="listRef" class="home-list" :style="gridListStyle">
<MarketCard v-for="card in eventList" :key="card.id" :id="card.id" :slug="card.slug"
:market-title="card.marketTitle" :chance-value="card.chanceValue" :market-info="card.marketInfo"
:image-url="card.imageUrl" :category="card.category" :expires-at="card.expiresAt"
:display-type="card.displayType" :outcomes="card.outcomes" :yes-label="card.yesLabel"
:no-label="card.noLabel" :is-new="card.isNew" :market-id="card.marketId"
:clob-token-ids="card.clobTokenIds" :yes-price="card.yesPrice" :no-price="card.noPrice"
@open-trade="onCardOpenTrade" />
<div v-if="eventListLoading" class="home-list-empty home-list-loading">
<v-progress-circular indeterminate size="40" width="2" />
<span>{{ t('common.loading') }}</span>
</div>
<div v-else-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
{{ t('common.noData') }}
</div>
</div>
<div v-if="eventList.length > 0" class="load-more-footer">
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
<div v-if="loadingMore" class="load-more-indicator">
<v-progress-circular indeterminate size="24" width="2" />
<span>{{ t('common.loading') }}</span>
</div>
<div v-else-if="noMoreEvents" class="no-more-tip">{{ t('home.noMore') }}</div>
<v-btn v-else class="load-more-btn" variant="outlined" color="primary" :disabled="loadingMore"
@click="loadMore">
{{ t('home.loadMore') }}
</v-btn>
</div>
<div class="flex-row home-left-menu2-container">
<div class="left-menu2" :class="{ 'position-fixed': isFixed, 'position-absolute': isAbsolute }">
<div v-for="item in categoryLayers[1]" :key="item.id" @click="menuStore.onCategorySelect(1, item.id)"
class="left-menu2-item" :class="{ 'active': layerActiveValues[1] === item.id }">
<div class="item-icon"></div>
<div class="item-label">{{ item.label }}</div>
<div class="item-count">{{ 99 }}</div>
</div>
</v-pull-to-refresh>
</div>
<!-- 可滚动容器作为 v-pull-to-refresh 的父元素组件据此判断 scrollTop 仅在顶部时才响应下拉 -->
<div ref="scrollRef" class="home-list-scroll">
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
<div class="pull-to-refresh-inner">
<div ref="listRef" class="home-list" :style="gridListStyle">
<MarketCard v-for="card in eventList" :key="card.id" :id="card.id" :slug="card.slug"
:market-title="card.marketTitle" :chance-value="card.chanceValue" :market-info="card.marketInfo"
:image-url="card.imageUrl" :category="card.category" :expires-at="card.expiresAt"
:display-type="card.displayType" :outcomes="card.outcomes" :yes-label="card.yesLabel"
:no-label="card.noLabel" :is-new="card.isNew" :market-id="card.marketId"
:clob-token-ids="card.clobTokenIds" :yes-price="card.yesPrice" :no-price="card.noPrice"
@open-trade="onCardOpenTrade" />
<div v-if="eventListLoading" class="home-list-empty home-list-loading">
<v-progress-circular indeterminate size="40" width="2" />
<span>{{ t('common.loading') }}</span>
</div>
<div v-else-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
{{ t('common.noData') }}
</div>
</div>
<div v-if="eventList.length > 0" class="load-more-footer">
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
<div v-if="loadingMore" class="load-more-indicator">
<v-progress-circular indeterminate size="24" width="2" />
<span>{{ t('common.loading') }}</span>
</div>
<div v-else-if="noMoreEvents" class="no-more-tip">{{ t('home.noMore') }}</div>
<v-btn v-else class="load-more-btn" variant="outlined" color="primary" :disabled="loadingMore"
@click="loadMore">
{{ t('home.loadMore') }}
</v-btn>
</div>
</div>
</v-pull-to-refresh>
</div>
</div>
<AppFooter />
<!-- PC对话框手机底部 sheet直接显示交易表单 -->
<v-dialog v-if="!isMobile" v-model="tradeDialogOpen" max-width="420" scrollable
@ -117,6 +129,7 @@ import { useToastStore } from '../stores/toast'
import { useLocaleStore } from '../stores/locale'
import { useMenuStore } from '../stores/menu'
import { storeToRefs } from 'pinia'
import AppFooter from '../components/AppFooter.vue'
const { mobile } = useDisplay()
const { t } = useI18n()
@ -129,6 +142,37 @@ const { filterVisible } = menuStore
/** 标记是否为分类初始化阶段,用于 watch 时区分初始加载与用户切换 */
const isCategoryInitializing = ref(false)
const isFixed = ref(false)
const isAbsolute = ref(false)
const OFFSET = 100
onMounted(() => {
window.addEventListener('scroll', update)
update()
nextTick(() => {
console.log('home onMounted')
if (layerActiveValues.value.length !== 0) {
loadEvents(1, false)
} else {
}
})
})
onUnmounted(() => {
window.removeEventListener('scroll', update)
})
function update() {
const scrollTop = window.scrollY
const winH = window.innerHeight
const docH = document.documentElement.scrollHeight
const bottomDistance = docH - scrollTop - winH
isFixed.value = bottomDistance <= OFFSET && bottomDistance > 0
isAbsolute.value = bottomDistance <= 0
}
function doSearch(keyword: string) {
@ -217,7 +261,6 @@ const tradeDialogMarket = ref<{
yesPrice?: number
noPrice?: number
} | null>(null)
const scrollRef = ref<HTMLElement | null>(null)
function onCardOpenTrade(
side: 'yes' | 'no',
@ -403,6 +446,10 @@ async function loadEvents(page: number, append: boolean, keyword?: string) {
total: eventTotal.value,
pageSize: eventPageSize.value,
})
nextTick(() => {
})
} catch (e) {
console.log(e)
if (!append) eventList.value = []
@ -448,7 +495,6 @@ onBeforeRouteLeave(() => {
onMounted(() => {
// emit
// if (props.initialSearchExpanded) expandSearch()
loadCategory()
nextTick(() => {
const scrollEl = getMainScrollEl()
const sentinel = sentinelRef.value
@ -515,12 +561,13 @@ onActivated(() => {
<style scoped lang="scss">
.home-container {
max-width: 1440px;
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
min-height: calc(100vh - 112px);
padding: 0;
margin: 112px 0 0 0;
margin: 112px auto 0 auto;
}
.home-title {
@ -532,7 +579,6 @@ onActivated(() => {
.pull-to-refresh {
width: 100%;
margin-top: 8px;
min-height: 100%;
padding: 8px;
}
@ -543,8 +589,13 @@ onActivated(() => {
/* 不设固定高度与 overflow列表随页面窗口滚动便于 Vue Router scrollBehavior 自动恢复位置 */
.home-list-scroll {
width: 100%;
overflow-x: visible;
flex: 1;
overflow-y: auto; //
overflow-x: hidden;
@include gt1024 {
margin-left: 200px;
}
}
/* 列数由 JS 根据容器宽度与 CARD_MIN_WIDTH 连续计算,避免断点导致 6→4 跳变 */
@ -811,4 +862,59 @@ onActivated(() => {
flex-direction: column;
height: 100%;
}
.home-left-menu2-container {
flex: 1;
}
.position-fixed {
position: fixed;
top: 112px;
}
.left-menu2 {
height: calc(100vh - 112px);
width: 190px;
display: none;
flex-direction: column;
margin: 8px 8px 0 0;
flex-shrink: 0;
overflow-y: auto;
overflow-x: hidden;
position: absolute;
top: 112px;
@include gt1024 {
display: flex;
}
.left-menu2-item {
width: 100%;
display: flex;
align-items: center;
padding: 12px;
cursor: pointer;
border-radius: 8px;
.item-icon {
width: 24px;
height: 24px;
}
.item-label {
flex: 1;
}
.item-count {
flex-shrink: 0;
}
}
.active {
font-weight: 700;
background-color: #e5e5e5;
}
}
</style>

View File

@ -201,7 +201,7 @@ function goWallet() {
}
</script>
<style scoped>
<style scoped lang="scss">
.member-center-page {
min-height: 100vh;
background: #ffffff;
@ -209,6 +209,9 @@ function goWallet() {
justify-content: center;
align-items: flex-start;
padding: 0;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.member-screen {

View File

@ -6,13 +6,7 @@
<label class="avatar-wrap" :aria-label="t('profile.changeAvatar')">
<input type="file" accept="image/*" class="avatar-input" @change="onAvatarFileChange" />
<div class="avatar">
<v-progress-circular
v-if="avatarUploading"
indeterminate
size="32"
width="2"
class="avatar-loading"
/>
<v-progress-circular v-if="avatarUploading" indeterminate size="32" width="2" class="avatar-loading" />
<template v-else>
<img v-if="avatarImage" :src="avatarImage" :alt="displayName" class="avatar-img" />
<span v-else>{{ avatarText }}</span>
@ -64,18 +58,12 @@
<section class="card menu-card">
<div class="menu-title">{{ t('profile.accountSettings') }}</div>
<button
v-for="item in settingItems"
:key="item.label"
class="menu-item"
type="button"
@click="goSetting(item)"
>
<button v-for="item in settingItems" :key="item.label" class="menu-item" type="button" @click="goSetting(item)">
<span>{{ item.label }}</span>
<span v-if="item.action === 'locale'" class="menu-locale">{{ currentLocaleLabel }}</span>
<span v-else-if="item.action === 'wallet'" class="menu-locale">{{
walletAddressShort
}}</span>
}}</span>
<span v-else class="menu-arrow">&gt;</span>
</button>
</section>
@ -88,13 +76,8 @@
<v-dialog v-model="localeDialogOpen" max-width="360">
<v-card class="locale-dialog-card" rounded="xl" elevation="0">
<div class="locale-dialog-title">{{ t('profile.selectLanguage') }}</div>
<button
v-for="opt in localeStore.localeOptions"
:key="opt.value"
class="locale-option"
type="button"
@click="chooseLocale(opt.value)"
>
<button v-for="opt in localeStore.localeOptions" :key="opt.value" class="locale-option" type="button"
@click="chooseLocale(opt.value)">
<span>{{ opt.label }}</span>
<span v-if="opt.value === localeStore.currentLocale" class="locale-selected"></span>
</button>
@ -106,12 +89,7 @@
<div class="wallet-dialog-title">{{ t('profile.currentWalletAddress') }}</div>
<div class="wallet-dialog-address">{{ walletAddressText }}</div>
<div class="wallet-dialog-actions">
<button
class="wallet-copy-btn"
type="button"
:disabled="!walletAddress"
@click="copyWalletAddress"
>
<button class="wallet-copy-btn" type="button" :disabled="!walletAddress" @click="copyWalletAddress">
{{ t('profile.copyAddress') }}
</button>
</div>
@ -122,45 +100,21 @@
<v-card class="name-dialog-card" elevation="0">
<div class="name-dialog-header">
<h2 class="name-dialog-title">{{ t('profile.editNameTitle') }}</h2>
<v-btn
icon
variant="text"
class="name-dialog-close-btn"
:aria-label="t('deposit.close')"
@click="closeEditNameDialog"
>
<v-btn icon variant="text" class="name-dialog-close-btn" :aria-label="t('deposit.close')"
@click="closeEditNameDialog">
<v-icon size="18">mdi-close</v-icon>
</v-btn>
</div>
<v-card-text class="name-dialog-body">
<v-text-field
v-model="editingName"
:label="t('profile.newUserName')"
variant="outlined"
density="comfortable"
hide-details="auto"
:error-messages="nameError ? [nameError] : []"
:hint="t('profile.nameFormatHint')"
persistent-hint
class="name-dialog-field"
/>
<v-text-field v-model="editingName" :label="t('profile.newUserName')" variant="outlined" density="comfortable"
hide-details="auto" :error-messages="nameError ? [nameError] : []" :hint="t('profile.nameFormatHint')"
persistent-hint class="name-dialog-field" />
<div class="name-dialog-actions">
<button
class="name-dialog-cancel-btn"
type="button"
:disabled="isSaving"
@click="closeEditNameDialog"
>
<button class="name-dialog-cancel-btn" type="button" :disabled="isSaving" @click="closeEditNameDialog">
{{ t('profile.cancel') }}
</button>
<button class="name-dialog-save-btn" type="button" :disabled="isSaving" @click="saveName">
<v-progress-circular
v-if="isSaving"
indeterminate
size="18"
width="2"
class="save-btn-spinner"
/>
<v-progress-circular v-if="isSaving" indeterminate size="18" width="2" class="save-btn-spinner" />
<span v-else>{{ t('profile.save') }}</span>
</button>
</div>
@ -493,14 +447,17 @@ onMounted(() => {
})
</script>
<style scoped>
<style scoped lang="scss">
.profile-page {
min-height: 100vh;
background: #ffffff;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.profile-screen {

View File

@ -314,13 +314,16 @@ function openResult(item: SearchResultItem) {
}
</script>
<style scoped>
<style scoped lang="scss">
.search-page {
min-height: 100vh;
background: #fcfcfc;
display: flex;
justify-content: center;
padding: 0;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.search-screen {

View File

@ -21,10 +21,13 @@
import OrderBook from '../components/OrderBook.vue'
</script>
<style scoped>
<style scoped lang="scss">
.trade-container {
padding: 40px 20px;
min-height: 100vh;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.trade-header {

View File

@ -1,27 +1,18 @@
<template>
<v-container
fluid
class="trade-detail-container"
:class="{
'trade-detail-container--settled-bar': showSettledOutcomeBar,
}"
>
<v-container fluid class="trade-detail-container" :class="{
'trade-detail-container--settled-bar': showSettledOutcomeBar,
}">
<v-pull-to-refresh class="trade-detail-pull-refresh" @load="onRefresh">
<div class="trade-detail-pull-refresh-inner">
<!-- findPmEvent 请求中仅显示 loading -->
<v-card
v-if="detailLoading && !eventDetail"
class="trade-detail-loading-card"
elevation="0"
rounded="lg"
>
<v-card v-if="detailLoading && !eventDetail" class="trade-detail-loading-card" elevation="0" rounded="lg">
<div class="trade-detail-loading-placeholder">
<v-progress-circular indeterminate color="primary" size="48" />
<p>{{ t('common.loading') }}</p>
</div>
</v-card>
<v-row v-else align="stretch" no-gutters class="trade-detail-row">
<v-row v-else align="stretch" class="trade-detail-row" style="--v-col-gap-y: 0px;">
<!-- 左侧分时图 + 订单簿宽度弹性 -->
<v-col cols="12" class="chart-col">
<!-- 分时图卡片Polymarket 样式 -->
@ -34,29 +25,14 @@
<p v-if="detailError" class="chart-error">{{ detailError }}</p>
<div class="chart-controls-row">
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
<v-btn-group
v-if="cryptoSymbol"
variant="outlined"
density="compact"
divided
class="chart-mode-toggle"
>
<v-btn
:class="{ active: chartMode === 'yesno' }"
size="small"
icon
:aria-label="t('chart.yesnoTimeSeries')"
@click="setChartMode('yesno')"
>
<v-btn-group v-if="cryptoSymbol" variant="outlined" density="compact" divided
class="chart-mode-toggle">
<v-btn :class="{ active: chartMode === 'yesno' }" size="small" icon
:aria-label="t('chart.yesnoTimeSeries')" @click="setChartMode('yesno')">
<v-icon size="20">mdi-chart-timeline-variant</v-icon>
</v-btn>
<v-btn
:class="{ active: chartMode === 'crypto' }"
size="small"
class="chart-mode-crypto-btn"
:aria-label="t('chart.cryptoPrice')"
@click="setChartMode('crypto')"
>
<v-btn :class="{ active: chartMode === 'crypto' }" size="small" class="chart-mode-crypto-btn"
:aria-label="t('chart.cryptoPrice')" @click="setChartMode('crypto')">
<span class="chart-crypto-ticker-label">{{ cryptoSymbol.toUpperCase() }}</span>
</v-btn>
</v-btn-group>
@ -87,14 +63,9 @@
<span v-if="marketExpiresAt" class="chart-expires">| {{ marketExpiresAt }}</span>
</div>
<div class="chart-time-ranges">
<v-btn
v-for="r in timeRanges"
:key="r.value"
:class="['time-range-btn', { active: selectedTimeRange === r.value }]"
variant="text"
size="small"
@click="selectTimeRange(r.value)"
>
<v-btn v-for="r in timeRanges" :key="r.value"
:class="['time-range-btn', { active: selectedTimeRange === r.value }]" variant="text" size="small"
@click="selectTimeRange(r.value)">
{{ r.label }}
</v-btn>
</div>
@ -102,12 +73,7 @@
</v-card>
<!-- 移动端无右侧交易栏时展示已关闭说明 -->
<v-card
v-if="isMobile && currentMarketClosed"
class="market-closed-mobile-card"
elevation="0"
rounded="lg"
>
<v-card v-if="isMobile && currentMarketClosed" class="market-closed-mobile-card" elevation="0" rounded="lg">
<div class="market-closed-pane market-closed-pane--mobile">
<v-icon size="40" color="grey-darken-1">mdi-lock-outline</v-icon>
<h2 class="market-closed-pane-title">{{ t('trade.marketClosedTitle') }}</h2>
@ -117,11 +83,7 @@
<!-- 持仓 / 限价订单簿上方 -->
<v-card class="positions-orders-card" elevation="0" rounded="lg">
<v-tabs
v-model="positionsOrdersTab"
class="positions-orders-tabs"
density="comfortable"
>
<v-tabs v-model="positionsOrdersTab" class="positions-orders-tabs" density="comfortable">
<v-tab value="positions">{{ t('activity.myPositions') }}</v-tab>
<v-tab value="orders">{{ t('activity.openOrders') }}</v-tab>
</v-tabs>
@ -135,19 +97,10 @@
{{ t('activity.noPositionsInMarket') }}
</div>
<div v-else class="positions-list">
<div
v-for="pos in marketPositionsFiltered"
:key="pos.id"
class="position-row-item"
>
<div v-for="pos in marketPositionsFiltered" :key="pos.id" class="position-row-item">
<div class="position-row-header">
<div class="position-row-icon" :class="pos.iconClass">
<img
v-if="pos.imageUrl"
:src="pos.imageUrl"
alt=""
class="position-row-icon-img"
/>
<img v-if="pos.imageUrl" :src="pos.imageUrl" alt="" class="position-row-icon-img" />
<span v-else class="position-row-icon-char">{{
pos.iconChar || '•'
}}</span>
@ -167,17 +120,9 @@
</span>
<span class="position-shares">{{ pos.shares }}</span>
<span class="position-value">{{ pos.value }}</span>
<v-btn
variant="outlined"
size="small"
color="primary"
class="position-sell-btn"
:disabled="
currentMarketClosed ||
!(pos.availableSharesNum != null && pos.availableSharesNum > 0)
"
@click="openSellFromPosition(pos)"
>
<v-btn variant="outlined" size="small" color="primary" class="position-sell-btn" :disabled="currentMarketClosed ||
!(pos.availableSharesNum != null && pos.availableSharesNum > 0)
" @click="openSellFromPosition(pos)">
{{ t('trade.sell') }}
</v-btn>
</div>
@ -196,9 +141,7 @@
<div v-else class="orders-list">
<div v-for="ord in marketOpenOrders" :key="ord.id" class="order-row-item">
<div class="order-row-main">
<span
:class="['order-side-pill', ord.side === 'Yes' ? 'side-yes' : 'side-no']"
>
<span :class="['order-side-pill', ord.side === 'Yes' ? 'side-yes' : 'side-no']">
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
</span>
<span class="order-price">{{ ord.price }}</span>
@ -206,13 +149,8 @@
<span class="order-total">{{ ord.total }}</span>
</div>
<div class="order-row-actions">
<v-btn
variant="text"
size="small"
color="error"
:disabled="cancelOrderLoading || ord.fullyFilled"
@click="cancelMarketOrder(ord)"
>
<v-btn variant="text" size="small" color="error"
:disabled="cancelOrderLoading || ord.fullyFilled" @click="cancelMarketOrder(ord)">
{{ t('activity.cancelOrder') }}
</v-btn>
</div>
@ -224,57 +162,33 @@
<!-- Order Book Section -->
<v-card v-if="!currentMarketClosed" class="order-book-card" elevation="0" rounded="lg">
<OrderBook
:asks-yes="orderBookAsksYes"
:bids-yes="orderBookBidsYes"
:asks-no="orderBookAsksNo"
:bids-no="orderBookBidsNo"
:anchor-best-bid-yes="orderBookBestBidYesCents"
:anchor-lowest-ask-yes="orderBookLowestAskYesCents"
:anchor-best-bid-no="orderBookBestBidNoCents"
<OrderBook :asks-yes="orderBookAsksYes" :bids-yes="orderBookBidsYes" :asks-no="orderBookAsksNo"
:bids-no="orderBookBidsNo" :anchor-best-bid-yes="orderBookBestBidYesCents"
:anchor-lowest-ask-yes="orderBookLowestAskYesCents" :anchor-best-bid-no="orderBookBestBidNoCents"
:anchor-lowest-ask-no="orderBookLowestAskNoCents"
:outcome-price-anchor-yes-cents="orderBookOutcomeAnchorYesCents"
:outcome-price-anchor-no-cents="orderBookOutcomeAnchorNoCents"
:last-price-yes="clobLastPriceYes"
:last-price-no="clobLastPriceNo"
:spread-yes="clobSpreadYes"
:spread-no="clobSpreadNo"
:loading="clobLoading"
:connected="clobConnected"
:yes-label="yesLabel"
:no-label="noLabel"
/>
:outcome-price-anchor-no-cents="orderBookOutcomeAnchorNoCents" :last-price-yes="clobLastPriceYes"
:last-price-no="clobLastPriceNo" :spread-yes="clobSpreadYes" :spread-no="clobSpreadNo"
:loading="clobLoading" :connected="clobConnected" :yes-label="yesLabel" :no-label="noLabel" />
</v-card>
<!-- Comments / Top Holders / Activity与左侧图表订单簿同宽 -->
<v-card class="activity-card" elevation="0" rounded="lg">
<div class="rules-pane">
<div
v-if="!eventDetail?.description && !eventDetail?.resolutionSource"
class="placeholder-pane"
>
<div v-if="!eventDetail?.description && !eventDetail?.resolutionSource" class="placeholder-pane">
{{ t('activity.rulesEmpty') }}
</div>
<template v-else>
<div v-if="eventDetail?.description" class="rules-section">
<h3 class="rules-title">{{ t('activity.rulesDescription') }}</h3>
<div
class="rules-text"
:class="{
'rules-text--multi-collapsed':
rulesDescriptionCollapsible && !rulesDescriptionExpanded,
}"
>
<div class="rules-text" :class="{
'rules-text--multi-collapsed':
rulesDescriptionCollapsible && !rulesDescriptionExpanded,
}">
{{ eventDetail.description }}
</div>
<v-btn
v-if="rulesDescriptionCollapsible"
variant="text"
size="small"
density="compact"
class="rules-description-toggle"
@click="rulesDescriptionExpanded = !rulesDescriptionExpanded"
>
<v-btn v-if="rulesDescriptionCollapsible" variant="text" size="small" density="compact"
class="rules-description-toggle" @click="rulesDescriptionExpanded = !rulesDescriptionExpanded">
{{
rulesDescriptionExpanded
? t('eventMarkets.collapseDescription')
@ -284,13 +198,8 @@
</div>
<div v-if="eventDetail?.resolutionSource" class="rules-section">
<h3 class="rules-title">{{ t('activity.rulesSource') }}</h3>
<a
v-if="isResolutionSourceUrl"
:href="eventDetail.resolutionSource"
target="_blank"
rel="noopener noreferrer"
class="rules-link"
>
<a v-if="isResolutionSourceUrl" :href="eventDetail.resolutionSource" target="_blank"
rel="noopener noreferrer" class="rules-link">
{{ eventDetail.resolutionSource }}
<v-icon size="14">mdi-open-in-new</v-icon>
</a>
@ -309,13 +218,9 @@
<p class="market-closed-pane-desc">{{ t('trade.marketClosedDesc') }}</p>
</div>
<div v-else-if="tradeMarketPayload" class="trade-sidebar">
<TradeComponent
ref="tradeComponentRef"
:market="tradeMarketPayload"
:positions="tradePositionsForComponent"
@merge-success="onMergeSuccess"
@split-success="onSplitSuccess"
/>
<TradeComponent ref="tradeComponentRef" :market="tradeMarketPayload"
:positions="tradePositionsForComponent" @merge-success="onMergeSuccess"
@split-success="onSplitSuccess" />
</div>
</v-col>
@ -323,37 +228,19 @@
<template v-if="isMobile && tradeMarketPayload && !currentMarketClosed">
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
<div class="mobile-trade-bar">
<v-btn
class="mobile-bar-btn mobile-bar-yes"
variant="flat"
rounded="sm"
@click="openSheetWithOption('yes')"
>
<v-btn class="mobile-bar-btn mobile-bar-yes" variant="flat" rounded="sm"
@click="openSheetWithOption('yes')">
{{ yesLabel }} {{ yesPriceCents }}¢
</v-btn>
<v-btn
class="mobile-bar-btn mobile-bar-no"
variant="flat"
rounded="sm"
@click="openSheetWithOption('no')"
>
<v-btn class="mobile-bar-btn mobile-bar-no" variant="flat" rounded="sm"
@click="openSheetWithOption('no')">
{{ noLabel }} {{ noPriceCents }}¢
</v-btn>
<v-menu
v-model="mobileMenuOpen"
:close-on-content-click="true"
location="top"
transition="scale-transition"
>
<v-menu v-model="mobileMenuOpen" :close-on-content-click="true" location="top"
transition="scale-transition">
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
class="mobile-bar-more-btn"
variant="flat"
icon
rounded="pill"
aria-label="更多操作"
>
<v-btn v-bind="menuProps" class="mobile-bar-more-btn" variant="flat" icon rounded="pill"
aria-label="更多操作">
<v-icon size="20">mdi-dots-horizontal</v-icon>
</v-btn>
</template>
@ -368,51 +255,30 @@
</v-menu>
</div>
<v-bottom-sheet v-model="tradeSheetOpen" content-class="trade-detail-trade-sheet">
<TradeComponent
v-if="tradeSheetRenderContent"
ref="mobileTradeComponentRef"
:market="tradeMarketPayload"
:initial-option="tradeInitialOptionFromBar"
:initial-tab="tradeInitialTabFromBar"
:positions="tradePositionsForComponent"
embedded-in-sheet
@order-success="onOrderSuccess"
@merge-success="onMergeSuccess"
@split-success="onSplitSuccess"
/>
<TradeComponent v-if="tradeSheetRenderContent" ref="mobileTradeComponentRef" :market="tradeMarketPayload"
:initial-option="tradeInitialOptionFromBar" :initial-tab="tradeInitialTabFromBar"
:positions="tradePositionsForComponent" embedded-in-sheet @order-success="onOrderSuccess"
@merge-success="onMergeSuccess" @split-success="onSplitSuccess" />
</v-bottom-sheet>
</template>
<!-- 从持仓点击 Sell 弹出的交易组件桌面/移动端通用 -->
<v-dialog
v-model="sellDialogOpen"
max-width="420"
content-class="trade-detail-sell-dialog"
transition="dialog-transition"
>
<TradeComponent
v-if="sellDialogRenderContent"
:market="tradeMarketPayload"
:initial-option="sellInitialOption"
:initial-tab="'sell'"
:positions="tradePositionsForComponent"
@order-success="onSellOrderSuccess"
@merge-success="onMergeSuccess"
@split-success="onSplitSuccess"
/>
<v-dialog v-model="sellDialogOpen" max-width="420" content-class="trade-detail-sell-dialog"
transition="dialog-transition">
<TradeComponent v-if="sellDialogRenderContent" :market="tradeMarketPayload"
:initial-option="sellInitialOption" :initial-tab="'sell'" :positions="tradePositionsForComponent"
@order-success="onSellOrderSuccess" @merge-success="onMergeSuccess" @split-success="onSplitSuccess" />
</v-dialog>
</v-row>
</div>
</v-pull-to-refresh>
<!-- 已结算结算结果固定在视口底部滚动时仍可见 -->
<div
v-if="showSettledOutcomeBar"
class="trade-settled-outcome-bar"
role="status"
>
<div v-if="showSettledOutcomeBar" class="trade-settled-outcome-bar" role="status">
{{ t('trade.marketClosedOutcome', { outcome: settledOutcomeLabel }) }}
</div>
<AppFooter />
</v-container>
</template>
@ -571,10 +437,10 @@ async function loadEventDetail() {
marketsCount: ev?.markets?.length ?? 0,
firstMarket: ev?.markets?.[0]
? {
question: ev.markets[0].question,
outcomePrices: ev.markets[0].outcomePrices,
marketId: ev.markets[0].marketId,
}
question: ev.markets[0].question,
outcomePrices: ev.markets[0].outcomePrices,
marketId: ev.markets[0].marketId,
}
: null,
})
} else {
@ -1303,10 +1169,10 @@ function ensureChartSeries() {
lastValueVisible: true,
priceFormat: isCrypto
? {
type: 'custom',
minMove: 1e-10,
formatter: (priceValue: BarPrice) => formatCryptoChartPrice(priceValue as number),
}
type: 'custom',
minMove: 1e-10,
formatter: (priceValue: BarPrice) => formatCryptoChartPrice(priceValue as number),
}
: { type: 'percent', precision: 1 },
priceScaleId: CHART_OVERLAY_PRICE_SCALE_ID,
})
@ -1650,13 +1516,14 @@ onUnmounted(() => {
})
</script>
<style scoped>
<style scoped lang="scss">
.trade-detail-container {
padding: 24px;
padding-left: 24px;
padding-right: 24px;
min-height: 100vh;
box-sizing: border-box;
max-width: 1440px;
margin: 0 auto;
margin-top: $header-height;
}
/* 底部固定展示结算结果时,为内容留出空间(高度与 .trade-settled-outcome-bar 一致) */
@ -2237,6 +2104,7 @@ onUnmounted(() => {
font-size: 14px;
padding: 24px 0;
}
.placeholder-pane--loading {
display: flex;
flex-direction: column;
@ -2476,10 +2344,12 @@ onUnmounted(() => {
}
@keyframes live-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
@ -2637,16 +2507,16 @@ onUnmounted(() => {
.mobile-bar-yes {
flex: 1;
min-width: 0;
background: #b8e0b8 !important;
color: #006600 !important;
background: $yes-bg;
color: $yes-text;
height: 46px;
}
.mobile-bar-no {
flex: 1;
min-width: 0;
background: #f0b8b8 !important;
color: #cc0000 !important;
background: $no-bg;
color: $no-text;
height: 46px;
}

View File

@ -1641,7 +1641,7 @@ async function submitAuthorize() {
}
</script>
<style scoped>
<style scoped lang="scss">
.wallet-container {
width: 100%;
max-width: 100%;
@ -1649,6 +1649,9 @@ async function submitAuthorize() {
padding: 16px;
background: #fcfcfc;
box-sizing: border-box;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height;
}
.wallet-mobile-frame {