优化样式调整
This commit is contained in:
parent
a21869d989
commit
e4ecea471c
80
src/App.vue
80
src/App.vue
@ -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>
|
||||
|
||||
@ -60,6 +60,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin lt1024 {
|
||||
@media screen and (max-width: 1025px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin gt1024 {
|
||||
@media screen and (min-width: 1024px) {
|
||||
@content;
|
||||
|
||||
@ -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;
|
||||
@ -36,3 +36,11 @@ button {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -22,3 +22,10 @@ $radius-lg: 12px;
|
||||
|
||||
// 阴影
|
||||
$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;
|
||||
@ -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> & <a href="#">Privacy Policy</a>.
|
||||
<a href="/doc/tos">Terms of Service</a> & <a href="/doc/privacy">Privacy Policy</a>.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -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 重叠 */
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
/** 按市场依次请求 getPmPriceHistoryPublic,market 传 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
|
||||
}) {
|
||||
// 可在此调用下单 API,payload 含 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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">></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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user