Compare commits
No commits in common. "2174abc9d37da5a92319b1d5ddec8a60c163abf7" and "0aa04471f1b2306ca613aebdbba24b3a0eb7a2c2" have entirely different histories.
2174abc9d3
...
0aa04471f1
@ -1,11 +1,4 @@
|
||||
import { get } from './request'
|
||||
import {
|
||||
DEFAULT_CATEGORY_ICON,
|
||||
resolveCategoryIcon,
|
||||
resolveCategoryIconColor,
|
||||
} from './categoryIcons'
|
||||
|
||||
export { DEFAULT_CATEGORY_ICON, resolveCategoryIconColor }
|
||||
|
||||
/**
|
||||
* 接口返回的 PmTag 结构(definitions polymarket.PmTag)
|
||||
@ -29,8 +22,6 @@ export interface PmTagMainItem {
|
||||
icon?: string
|
||||
sectionTitle?: string
|
||||
forceHide?: boolean
|
||||
/** 排序值,有则按从小到大排序 */
|
||||
sort?: number
|
||||
}
|
||||
|
||||
/** 分类树节点(与后端返回结构一致) */
|
||||
@ -48,8 +39,6 @@ export interface CategoryTreeNode {
|
||||
updatedBy?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
/** 排序值,有则按从小到大排序 */
|
||||
sort?: number
|
||||
children?: CategoryTreeNode[]
|
||||
}
|
||||
|
||||
@ -119,89 +108,58 @@ export interface CategoryTreeResponse {
|
||||
msg: string
|
||||
}
|
||||
|
||||
/** 递归为树节点补充图标(无 icon 时自动匹配),可导出供 mock 等场景使用 */
|
||||
export function enrichWithIcons(nodes: CategoryTreeNode[]): CategoryTreeNode[] {
|
||||
return nodes.map((n) => {
|
||||
const icon = n.icon ?? resolveCategoryIcon(n)
|
||||
const children = n.children?.length ? enrichWithIcons(n.children) : undefined
|
||||
return { ...n, icon: icon ?? n.icon, children }
|
||||
})
|
||||
/**
|
||||
* 获取分类树(公开接口,不需要鉴权)
|
||||
* GET /PmTag/getPmTagPublic
|
||||
*
|
||||
* 返回带 children 的树形结构,最多三层
|
||||
* data 可能为数组,或 { list: [] } 等格式,统一转为 CategoryTreeNode[]
|
||||
*/
|
||||
export async function getCategoryTree(): Promise<CategoryTreeResponse> {
|
||||
const res = await get<{
|
||||
code: number
|
||||
data: CategoryTreeNode[] | { list?: CategoryTreeNode[] }
|
||||
msg: string
|
||||
}>('/PmTag/getPmTagPublic')
|
||||
let data: CategoryTreeNode[] = []
|
||||
const raw = res.data
|
||||
if (Array.isArray(raw)) {
|
||||
data = raw
|
||||
} else if (raw && typeof raw === 'object') {
|
||||
if (Array.isArray((raw as { list?: CategoryTreeNode[] }).list)) {
|
||||
data = (raw as { list: CategoryTreeNode[] }).list
|
||||
} else if ((raw as CategoryTreeNode).id && (raw as CategoryTreeNode).label) {
|
||||
data = [raw as CategoryTreeNode]
|
||||
}
|
||||
}
|
||||
return { code: res.code, data, msg: res.msg }
|
||||
}
|
||||
|
||||
/** 递归按 sort 字段从小到大排序(有 sort 的节点排前面,同层比较) */
|
||||
function sortBySortField<T extends { sort?: number; children?: T[] }>(nodes: T[]): T[] {
|
||||
const hasSort = nodes.some((n) => n.sort != null && Number.isFinite(n.sort))
|
||||
if (!hasSort) return nodes
|
||||
return [...nodes]
|
||||
.sort((a, b) => {
|
||||
const sa = a.sort
|
||||
const sb = b.sort
|
||||
if (sa != null && sb != null) return sa - sb
|
||||
if (sa != null) return -1
|
||||
if (sb != null) return 1
|
||||
return 0
|
||||
})
|
||||
.map((n) => {
|
||||
if (n.children?.length) {
|
||||
return { ...n, children: sortBySortField(n.children) }
|
||||
}
|
||||
return n
|
||||
})
|
||||
}
|
||||
|
||||
/** 将 PmTagMainItem 转为 CategoryTreeNode,无 icon 时自动匹配 MDI 图标 */
|
||||
/** 将 PmTagMainItem 转为 CategoryTreeNode */
|
||||
function mapPmTagToTreeNode(item: PmTagMainItem): CategoryTreeNode {
|
||||
const rawId = item.ID
|
||||
const id = rawId != null ? String(rawId) : (item.slug ?? item.label ?? '')
|
||||
const children = Array.isArray(item.children) ? item.children.map(mapPmTagToTreeNode) : undefined
|
||||
const icon = item.icon ?? resolveCategoryIcon({ label: item.label, slug: item.slug })
|
||||
return {
|
||||
id,
|
||||
label: item.label ?? '',
|
||||
slug: item.slug ?? '',
|
||||
icon,
|
||||
icon: item.icon,
|
||||
sectionTitle: item.sectionTitle,
|
||||
forceShow: item.forceShow,
|
||||
forceHide: item.forceHide,
|
||||
sort: item.sort,
|
||||
children: children?.length ? children : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** getPmTagList 响应:data 可能为 { All: PmTag[] } 或 PmTag[] */
|
||||
type GetPmTagListData = PmTagMainItem[] | { All?: PmTagMainItem[] }
|
||||
|
||||
/**
|
||||
* 获取主分类(首页顶部分类 Tab 数据)
|
||||
* GET /PmTag/getPmTagList(原 getPmTagMain)
|
||||
* GET /PmTag/getPmTagMain
|
||||
*
|
||||
* 数据结构:三层结构均在 data.All 下,data.All 为根节点数组
|
||||
* 不需要鉴权,返回带 children 的树形结构,最多三层
|
||||
*/
|
||||
export async function getPmTagMain(): Promise<CategoryTreeResponse> {
|
||||
const res = await get<{ code: number; data: GetPmTagListData; msg: string }>(
|
||||
'/PmTag/getPmTagList'
|
||||
)
|
||||
// 调试:打印接口原始数据
|
||||
console.log('[getPmTagList] 原始响应:', JSON.stringify(res, null, 2))
|
||||
let raw: PmTagMainItem[] = []
|
||||
const d = res.data
|
||||
if (d && typeof d === 'object' && !Array.isArray(d) && 'All' in d) {
|
||||
raw = Array.isArray((d as { All?: PmTagMainItem[] }).All)
|
||||
? (d as { All: PmTagMainItem[] }).All
|
||||
: []
|
||||
} else if (Array.isArray(d)) {
|
||||
raw = d
|
||||
}
|
||||
let mapped = raw.map(mapPmTagToTreeNode)
|
||||
mapped = sortBySortField(mapped)
|
||||
// 第一层不显示,取 All 菜单下的三层作为展示数据(政治、体育、加密等)
|
||||
const allNode = mapped.find(
|
||||
(n) =>
|
||||
n.slug?.toLowerCase() === 'all' ||
|
||||
n.label === 'All' ||
|
||||
n.label === '全部'
|
||||
)
|
||||
const children = allNode?.children ?? mapped
|
||||
const data: CategoryTreeNode[] = enrichWithIcons(sortBySortField(children))
|
||||
const res = await get<{ code: number; data: PmTagMainItem[]; msg: string }>('/PmTag/getPmTagMain')
|
||||
const data: CategoryTreeNode[] = Array.isArray(res.data) ? res.data.map(mapPmTagToTreeNode) : []
|
||||
return { code: res.code, data, msg: res.msg }
|
||||
}
|
||||
|
||||
@ -1,212 +0,0 @@
|
||||
/**
|
||||
* 第二层分类图标自动匹配
|
||||
* 基于 label/slug 从 MDI(Material Design Icons)查找合适图标
|
||||
* 项目已引入 @mdi/font,图标名为 mdi-xxx 格式
|
||||
*/
|
||||
|
||||
/** slug/label(小写) -> MDI 图标名 */
|
||||
const ICON_MAP: Record<string, string> = {
|
||||
// 通用
|
||||
all: 'mdi-view-grid-outline',
|
||||
'全部': 'mdi-view-grid-outline',
|
||||
latest: 'mdi-clock-outline',
|
||||
'最新': 'mdi-new-box',
|
||||
new: 'mdi-new-box',
|
||||
trending: 'mdi-trending-up',
|
||||
|
||||
// 政治
|
||||
politics: 'mdi-gavel',
|
||||
'政治': 'mdi-gavel',
|
||||
election: 'mdi-vote-outline',
|
||||
elections: 'mdi-vote-outline',
|
||||
'选举': 'mdi-vote-outline',
|
||||
|
||||
// 体育
|
||||
sports: 'mdi-basketball',
|
||||
'体育': 'mdi-basketball',
|
||||
football: 'mdi-soccer',
|
||||
'足球': 'mdi-soccer',
|
||||
basketball: 'mdi-basketball',
|
||||
'篮球': 'mdi-basketball',
|
||||
nba: 'mdi-basketball',
|
||||
macro: 'mdi-earth',
|
||||
|
||||
// 加密 / 金融
|
||||
crypto: 'mdi-currency-btc',
|
||||
'加密': 'mdi-currency-btc',
|
||||
btc: 'mdi-currency-btc',
|
||||
bitcoin: 'mdi-currency-btc',
|
||||
eth: 'mdi-currency-eth',
|
||||
ethereum: 'mdi-currency-eth',
|
||||
finance: 'mdi-chart-line',
|
||||
'财务': 'mdi-chart-line',
|
||||
'金融': 'mdi-chart-line',
|
||||
'fed rates': 'mdi-chart-line-variant',
|
||||
fedrates: 'mdi-chart-line-variant',
|
||||
|
||||
// 地缘 / 世界
|
||||
geopolitics: 'mdi-earth',
|
||||
'地缘政治': 'mdi-earth',
|
||||
world: 'mdi-earth',
|
||||
'世界': 'mdi-earth',
|
||||
|
||||
// 加密子类:Above/Below、Up/Down、时间周期
|
||||
'above-below': 'mdi-arrow-up-down',
|
||||
'up-down': 'mdi-arrow-up-down',
|
||||
'5m': 'mdi-clock-outline',
|
||||
'15m': 'mdi-clock-outline',
|
||||
'1h': 'mdi-refresh',
|
||||
'4h': 'mdi-clock-outline',
|
||||
'1d': 'mdi-calendar',
|
||||
'5分钟': 'mdi-clock-outline',
|
||||
'15分钟': 'mdi-clock-outline',
|
||||
'每小时': 'mdi-refresh',
|
||||
'4小时': 'mdi-clock-outline',
|
||||
'每天': 'mdi-calendar',
|
||||
|
||||
// 文化 / 科技
|
||||
culture: 'mdi-palette-outline',
|
||||
'文化': 'mdi-palette-outline',
|
||||
tech: 'mdi-laptop',
|
||||
'科技': 'mdi-laptop',
|
||||
ai: 'mdi-robot-outline',
|
||||
climate: 'mdi-weather-partly-cloudy',
|
||||
'气候': 'mdi-weather-partly-cloudy',
|
||||
science: 'mdi-flask-outline',
|
||||
'科学': 'mdi-flask-outline',
|
||||
|
||||
// Polymarket 风格(参考官网 dashboards)
|
||||
trump: 'mdi-account',
|
||||
}
|
||||
|
||||
/** 无匹配时的默认图标 */
|
||||
export const DEFAULT_CATEGORY_ICON = 'mdi-tag-outline'
|
||||
|
||||
/** slug/label -> 图标颜色(Vuetify 颜色名或 CSS 颜色值) */
|
||||
const ICON_COLOR_MAP: Record<string, string> = {
|
||||
politics: 'primary',
|
||||
'政治': 'primary',
|
||||
election: 'primary',
|
||||
elections: 'primary',
|
||||
sports: 'success',
|
||||
'体育': 'success',
|
||||
football: 'success',
|
||||
'足球': 'success',
|
||||
basketball: 'success',
|
||||
'篮球': 'success',
|
||||
crypto: 'warning',
|
||||
'加密': 'warning',
|
||||
btc: 'warning',
|
||||
eth: 'info',
|
||||
finance: 'info',
|
||||
'财务': 'info',
|
||||
'金融': 'info',
|
||||
geopolitics: 'secondary',
|
||||
'地缘政治': 'secondary',
|
||||
world: 'secondary',
|
||||
culture: 'purple',
|
||||
'文化': 'purple',
|
||||
tech: 'info',
|
||||
'科技': 'info',
|
||||
ai: 'secondary',
|
||||
climate: 'success',
|
||||
'气候': 'success',
|
||||
latest: 'primary',
|
||||
'最新': 'primary',
|
||||
all: 'grey',
|
||||
'全部': 'grey',
|
||||
}
|
||||
|
||||
/** 关键词 -> 颜色 */
|
||||
const LABEL_COLOR_KEYWORDS: [string | RegExp, string][] = [
|
||||
['政治', 'primary'],
|
||||
['选举', 'primary'],
|
||||
['足球', 'success'],
|
||||
['篮球', 'success'],
|
||||
['体育', 'success'],
|
||||
['加密', 'warning'],
|
||||
['比特', 'warning'],
|
||||
['以太', 'info'],
|
||||
['财务', 'info'],
|
||||
['金融', 'info'],
|
||||
['地缘', 'secondary'],
|
||||
['世界', 'secondary'],
|
||||
['文化', 'purple'],
|
||||
['科技', 'info'],
|
||||
['气候', 'success'],
|
||||
]
|
||||
|
||||
/** 关键词(label 包含) -> 图标 */
|
||||
const LABEL_KEYWORDS: [string | RegExp, string][] = [
|
||||
['政治', 'mdi-gavel'],
|
||||
['选举', 'mdi-vote-outline'],
|
||||
['足球', 'mdi-soccer'],
|
||||
['篮球', 'mdi-basketball'],
|
||||
['体育', 'mdi-basketball'],
|
||||
['加密', 'mdi-currency-btc'],
|
||||
['比特', 'mdi-currency-btc'],
|
||||
['以太', 'mdi-currency-eth'],
|
||||
['财务', 'mdi-chart-line'],
|
||||
['金融', 'mdi-chart-line'],
|
||||
['地缘', 'mdi-earth'],
|
||||
['世界', 'mdi-earth'],
|
||||
['全部', 'mdi-view-grid-outline'],
|
||||
['最新', 'mdi-new-box'],
|
||||
['文化', 'mdi-palette-outline'],
|
||||
['科技', 'mdi-laptop'],
|
||||
['气候', 'mdi-weather-partly-cloudy'],
|
||||
['科学', 'mdi-flask-outline'],
|
||||
[/^\d+[mhd]$/i, 'mdi-clock-outline'],
|
||||
[/分钟|小时|每天/, 'mdi-clock-outline'],
|
||||
]
|
||||
|
||||
function normalizeKey(s: string): string {
|
||||
return s.trim().toLowerCase().replace(/\s+/g, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 label 或 slug 解析 MDI 图标
|
||||
* 优先精确匹配 slug,其次 label,最后关键词匹配;无匹配时返回默认图标
|
||||
*/
|
||||
export function resolveCategoryIcon(node: { label?: string; slug?: string; icon?: string }): string {
|
||||
if (node.icon && node.icon.startsWith('mdi-')) return node.icon
|
||||
|
||||
const slug = node.slug ? normalizeKey(node.slug) : ''
|
||||
const label = node.label?.trim() ?? ''
|
||||
|
||||
// 1. 精确匹配 slug
|
||||
const bySlug = slug && ICON_MAP[slug]
|
||||
if (bySlug) return bySlug
|
||||
|
||||
// 2. 精确匹配 label
|
||||
const byLabel = label && (ICON_MAP[label] ?? ICON_MAP[normalizeKey(label)])
|
||||
if (byLabel) return byLabel
|
||||
|
||||
// 3. 关键词匹配 label
|
||||
for (const [keyword, icon] of LABEL_KEYWORDS) {
|
||||
if (typeof keyword === 'string' && label.includes(keyword)) return icon
|
||||
if (keyword instanceof RegExp && keyword.test(label)) return icon
|
||||
}
|
||||
|
||||
return DEFAULT_CATEGORY_ICON
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 label 或 slug 解析图标颜色
|
||||
* 返回 Vuetify 颜色名(primary/success/warning 等)或 CSS 颜色,无匹配时返回 undefined(继承默认色)
|
||||
*/
|
||||
export function resolveCategoryIconColor(node: { label?: string; slug?: string }): string | undefined {
|
||||
const slug = node.slug ? normalizeKey(node.slug) : ''
|
||||
const label = node.label?.trim() ?? ''
|
||||
|
||||
if (slug && ICON_COLOR_MAP[slug]) return ICON_COLOR_MAP[slug]
|
||||
if (label && ICON_COLOR_MAP[label]) return ICON_COLOR_MAP[label]
|
||||
if (label && ICON_COLOR_MAP[normalizeKey(label)]) return ICON_COLOR_MAP[normalizeKey(label)]
|
||||
|
||||
for (const [keyword, color] of LABEL_COLOR_KEYWORDS) {
|
||||
if (typeof keyword === 'string' && label.includes(keyword)) return color
|
||||
if (keyword instanceof RegExp && keyword.test(label)) return color
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@ -121,22 +121,19 @@ export interface GetPmEventListParams {
|
||||
createdAtRange?: string[]
|
||||
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
|
||||
tokenid?: string | string[]
|
||||
/** 标签 ID,按分类筛选 */
|
||||
tagId?: number
|
||||
/** 标签 slug,按分类筛选 */
|
||||
tagSlug?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||||
* GET /PmEvent/getPmEventPublic
|
||||
*
|
||||
* Query: page, pageSize, keyword, createdAtRange, tokenid, tagId, tagSlug
|
||||
* Query: page, pageSize, keyword, createdAtRange, tokenid
|
||||
* tokenid 对应 market.clobTokenIds 中的值,可传单个或数组
|
||||
*/
|
||||
export async function getPmEventPublic(
|
||||
params: GetPmEventListParams = {},
|
||||
): Promise<PmEventListResponse> {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid, tagId, tagSlug } = params
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params
|
||||
const query: Record<string, string | number | string[] | undefined> = {
|
||||
page,
|
||||
pageSize,
|
||||
@ -146,8 +143,6 @@ export async function getPmEventPublic(
|
||||
if (tokenid != null) {
|
||||
query.tokenid = Array.isArray(tokenid) ? tokenid : [tokenid]
|
||||
}
|
||||
if (tagId != null && Number.isFinite(tagId)) query.tagId = tagId
|
||||
if (tagSlug != null && tagSlug !== '') query.tagSlug = tagSlug
|
||||
|
||||
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||
}
|
||||
|
||||
@ -182,7 +182,6 @@
|
||||
hide-details
|
||||
density="compact"
|
||||
@update:model-value="onLimitPriceInput"
|
||||
@blur="onLimitPriceBlur"
|
||||
@keydown="onLimitPriceKeydown"
|
||||
@paste="onLimitPricePaste"
|
||||
></v-text-field>
|
||||
@ -440,7 +439,6 @@
|
||||
hide-details
|
||||
density="compact"
|
||||
@update:model-value="onLimitPriceInput"
|
||||
@blur="onLimitPriceBlur"
|
||||
@keydown="onLimitPriceKeydown"
|
||||
@paste="onLimitPricePaste"
|
||||
></v-text-field>
|
||||
@ -938,42 +936,6 @@ import { OrderType, Side } from '../api/constants'
|
||||
const { mobile } = useDisplay()
|
||||
const userStore = useUserStore()
|
||||
|
||||
/** 限价单允许的 135 个价格档位(0–1 区间),规则:1–9/10–90/100–9900/9910–9990/9991–9999 */
|
||||
function buildAllowedLimitPrices(): number[] {
|
||||
const list: number[] = []
|
||||
for (let i = 1; i <= 9; i++) list.push(i / 10000)
|
||||
for (let i = 10; i <= 90; i += 10) list.push(i / 10000)
|
||||
for (let i = 100; i <= 9900; i += 100) list.push(i / 10000)
|
||||
for (let i = 9910; i <= 9990; i += 10) list.push(i / 10000)
|
||||
for (let i = 9991; i <= 9999; i++) list.push(i / 10000)
|
||||
return list
|
||||
}
|
||||
const ALLOWED_LIMIT_PRICES = buildAllowedLimitPrices()
|
||||
|
||||
/** 将限价吸附到最近的允许档位 */
|
||||
function snapToAllowedPrice(v: number): number {
|
||||
const clamped = Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0))
|
||||
const list = ALLOWED_LIMIT_PRICES
|
||||
if (list.length === 0) return clamped
|
||||
let nearest = list[0] as number
|
||||
let minDiff = Math.abs(clamped - nearest)
|
||||
for (const p of list) {
|
||||
const d = Math.abs(clamped - p)
|
||||
if (d < minDiff) {
|
||||
minDiff = d
|
||||
nearest = p
|
||||
}
|
||||
}
|
||||
return nearest
|
||||
}
|
||||
|
||||
/** 获取当前价格在允许列表中的索引,-1 表示不在列表中 */
|
||||
function indexOfAllowedPrice(v: number): number {
|
||||
const snapped = snapToAllowedPrice(v)
|
||||
const idx = ALLOWED_LIMIT_PRICES.findIndex((p) => Math.abs(p - snapped) < 1e-9)
|
||||
return idx >= 0 ? idx : 0
|
||||
}
|
||||
|
||||
export interface TradeMarketPayload {
|
||||
marketId?: string
|
||||
yesPrice: number
|
||||
@ -1067,8 +1029,6 @@ async function submitSplit() {
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openMergeDialog, openSplitDialog })
|
||||
|
||||
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
|
||||
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
|
||||
|
||||
@ -1129,9 +1089,8 @@ function applyInitialOption(option: 'yes' | 'no') {
|
||||
syncLimitPriceFromMarket()
|
||||
}
|
||||
|
||||
/** 限价限制到 [0,1] 并吸附到 135 个允许档位之一 */
|
||||
function clampLimitPrice(v: number): number {
|
||||
return snapToAllowedPrice(Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0)))
|
||||
return Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0))
|
||||
}
|
||||
|
||||
/** 根据当前 props.market 与 selectedOption 同步 limitPrice(组件显示或 market 更新时调用) */
|
||||
@ -1174,16 +1133,11 @@ const handleOptionChange = (option: 'yes' | 'no') => {
|
||||
emit('optionChange', option)
|
||||
}
|
||||
|
||||
/** 仅接受 135 个允许档位:输入值吸附到最近档位,非法值忽略 */
|
||||
/** 仅在值在 [0,1] 且为有效数字时更新,否则保持原值不变 */
|
||||
function onLimitPriceInput(v: unknown) {
|
||||
const num = v == null ? NaN : Number(v)
|
||||
if (!Number.isFinite(num) || num < 0 || num > 1) return
|
||||
limitPrice.value = snapToAllowedPrice(num)
|
||||
}
|
||||
|
||||
/** 失焦时吸附到允许档位 */
|
||||
function onLimitPriceBlur() {
|
||||
limitPrice.value = snapToAllowedPrice(limitPrice.value)
|
||||
limitPrice.value = num
|
||||
}
|
||||
|
||||
/** 只允许数字和小数点输入 */
|
||||
@ -1208,17 +1162,13 @@ function onLimitPricePaste(e: ClipboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 限价调整方法:在 135 个允许档位间上下切换
|
||||
// 限价调整方法(0–1 区间)
|
||||
const decreasePrice = () => {
|
||||
const idx = indexOfAllowedPrice(limitPrice.value)
|
||||
const nextIdx = Math.max(0, idx - 1)
|
||||
limitPrice.value = ALLOWED_LIMIT_PRICES[nextIdx] ?? limitPrice.value
|
||||
limitPrice.value = clampLimitPrice(limitPrice.value - 0.01)
|
||||
}
|
||||
|
||||
const increasePrice = () => {
|
||||
const idx = indexOfAllowedPrice(limitPrice.value)
|
||||
const nextIdx = Math.min(ALLOWED_LIMIT_PRICES.length - 1, idx + 1)
|
||||
limitPrice.value = ALLOWED_LIMIT_PRICES[nextIdx] ?? limitPrice.value
|
||||
limitPrice.value = clampLimitPrice(limitPrice.value + 0.01)
|
||||
}
|
||||
|
||||
/** 将 shares 限制为正整数(>= 1) */
|
||||
|
||||
@ -83,19 +83,17 @@
|
||||
class="buy-yes-btn"
|
||||
variant="flat"
|
||||
size="small"
|
||||
rounded="sm"
|
||||
@click="openTrade(market, index, 'yes')"
|
||||
>
|
||||
Yes {{ yesPrice(market) }}
|
||||
Buy Yes {{ yesPrice(market) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="buy-no-btn"
|
||||
variant="flat"
|
||||
size="small"
|
||||
rounded="sm"
|
||||
@click="openTrade(market, index, 'no')"
|
||||
>
|
||||
No {{ noPrice(market) }}
|
||||
Buy No {{ noPrice(market) }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,8 +101,8 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- 右侧:购买组件(桌面端显示;移动端用底部栏+弹窗) -->
|
||||
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
||||
<!-- 右侧:购买组件(点击 Yes/No 时传入当前市场数据) -->
|
||||
<v-col cols="12" class="trade-col">
|
||||
<div v-if="markets.length > 0" class="trade-sidebar">
|
||||
<TradeComponent
|
||||
:market="tradeMarketPayload"
|
||||
@ -114,69 +112,6 @@
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 移动端:单 market 时显示固定底部 Yes/No 栏 + 三点菜单(Merge/Split) -->
|
||||
<template v-if="isMobile && markets.length === 1">
|
||||
<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')"
|
||||
>
|
||||
Yes {{ barMarket ? yesPrice(barMarket) : '0¢' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mobile-bar-btn mobile-bar-no"
|
||||
variant="flat"
|
||||
rounded="sm"
|
||||
@click="openSheetWithOption('no')"
|
||||
>
|
||||
No {{ barMarket ? noPrice(barMarket) : '0¢' }}
|
||||
</v-btn>
|
||||
<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-icon size="20">mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="openMergeFromBar">
|
||||
<v-list-item-title>Merge</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="openSplitFromBar">
|
||||
<v-list-item-title>Split</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 移动端交易弹窗:单 market 与多 market 均需(多 market 时通过列表 Buy Yes/No 打开) -->
|
||||
<template v-if="isMobile && markets.length > 0">
|
||||
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
|
||||
<TradeComponent
|
||||
ref="tradeComponentRef"
|
||||
:key="`trade-${selectedMarketIndex}-${tradeInitialOption}`"
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
embedded-in-sheet
|
||||
@submit="onTradeSubmit"
|
||||
/>
|
||||
</v-bottom-sheet>
|
||||
</template>
|
||||
</template>
|
||||
</v-container>
|
||||
</template>
|
||||
@ -185,7 +120,6 @@
|
||||
defineOptions({ name: 'EventMarkets' })
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts } from 'echarts'
|
||||
import TradeComponent from '../components/TradeComponent.vue'
|
||||
@ -202,8 +136,6 @@ import { useUserStore } from '../stores/user'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { mobile } = useDisplay()
|
||||
const isMobile = computed(() => mobile.value)
|
||||
|
||||
const eventDetail = ref<PmEventListItem | null>(null)
|
||||
const detailLoading = ref(false)
|
||||
@ -212,19 +144,11 @@ const detailError = ref<string | null>(null)
|
||||
const selectedMarketIndex = ref(0)
|
||||
/** 点击 Buy Yes/No 时传给购买组件的初始方向,不点击则为 undefined 使用组件默认 */
|
||||
const tradeInitialOption = ref<'yes' | 'no' | undefined>(undefined)
|
||||
/** 移动端交易弹窗开关 */
|
||||
const tradeSheetOpen = ref(false)
|
||||
/** 移动端底部栏三点菜单开关 */
|
||||
const mobileMenuOpen = ref(false)
|
||||
/** TradeComponent 引用,用于从底部栏触发 Merge/Split */
|
||||
const tradeComponentRef = ref<InstanceType<typeof TradeComponent> | null>(null)
|
||||
const markets = computed(() => {
|
||||
const list = eventDetail.value?.markets ?? []
|
||||
return list.length > 0 ? list : []
|
||||
})
|
||||
const selectedMarket = computed(() => markets.value[selectedMarketIndex.value] ?? null)
|
||||
/** 移动端底部栏显示的市场(选中项或首个),仅在 markets.length > 0 时使用 */
|
||||
const barMarket = computed(() => selectedMarket.value ?? markets.value[0])
|
||||
/** 传给购买组件的市场数据(当前选中的市场) */
|
||||
const tradeMarketPayload = computed(() => {
|
||||
const m = selectedMarket.value
|
||||
@ -512,37 +436,10 @@ function selectMarket(index: number) {
|
||||
selectedMarketIndex.value = index
|
||||
}
|
||||
|
||||
/** 点击 Buy Yes/No:选中该市场并把数据和方向传给购买组件;移动端直接弹出交易弹窗 */
|
||||
/** 点击 Buy Yes/No:选中该市场并把数据和方向传给购买组件,不跳转 */
|
||||
function openTrade(market: PmEventMarketItem, index: number, side: 'yes' | 'no') {
|
||||
selectedMarketIndex.value = index
|
||||
tradeInitialOption.value = side
|
||||
if (isMobile.value) {
|
||||
tradeSheetOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/** 移动端底部栏:点击 Yes/No 时打开交易弹窗 */
|
||||
function openSheetWithOption(side: 'yes' | 'no') {
|
||||
tradeInitialOption.value = side
|
||||
tradeSheetOpen.value = true
|
||||
}
|
||||
|
||||
/** 从底部栏三点菜单打开 Merge 弹窗 */
|
||||
function openMergeFromBar() {
|
||||
mobileMenuOpen.value = false
|
||||
tradeSheetOpen.value = true
|
||||
nextTick(() => {
|
||||
tradeComponentRef.value?.openMergeDialog?.()
|
||||
})
|
||||
}
|
||||
|
||||
/** 从底部栏三点菜单打开 Split 弹窗 */
|
||||
function openSplitFromBar() {
|
||||
mobileMenuOpen.value = false
|
||||
tradeSheetOpen.value = true
|
||||
nextTick(() => {
|
||||
tradeComponentRef.value?.openSplitDialog?.()
|
||||
})
|
||||
}
|
||||
|
||||
function onTradeSubmit(payload: {
|
||||
@ -946,98 +843,21 @@ watch(
|
||||
|
||||
.market-row-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 宽屏时按钮与问题同行,节省垂直空间 */
|
||||
@media (min-width: 768px) {
|
||||
.market-row-actions {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.buy-yes-btn,
|
||||
.buy-no-btn {
|
||||
flex: 1;
|
||||
min-width: 72px;
|
||||
max-width: 140px;
|
||||
white-space: nowrap;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.buy-yes-btn {
|
||||
background-color: #b8e0b8 !important;
|
||||
color: #006600 !important;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.buy-no-btn {
|
||||
background-color: #f0b8b8 !important;
|
||||
color: #cc0000 !important;
|
||||
}
|
||||
|
||||
/* 移动端底部交易栏 */
|
||||
.mobile-trade-bar-spacer {
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.mobile-trade-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.mobile-bar-btn {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.mobile-bar-yes {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #b8e0b8 !important;
|
||||
color: #006600 !important;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.mobile-bar-no {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #f0b8b8 !important;
|
||||
color: #cc0000 !important;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.mobile-bar-more-btn {
|
||||
flex-shrink: 0;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
min-width: 46px;
|
||||
min-height: 46px;
|
||||
padding: 0 !important;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb !important;
|
||||
color: #6b7280;
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -116,13 +116,8 @@
|
||||
:class="{ 'home-category-icon-item--active': layerActiveValues[1] === item.id }"
|
||||
@click="onCategorySelect(1, item.id)"
|
||||
>
|
||||
<v-icon
|
||||
size="24"
|
||||
class="home-category-icon"
|
||||
:color="resolveCategoryIconColor(item) ?? 'grey'"
|
||||
>
|
||||
{{ item.icon || DEFAULT_CATEGORY_ICON }}
|
||||
</v-icon>
|
||||
<v-icon v-if="item.icon" size="24" class="home-category-icon">{{ item.icon }}</v-icon>
|
||||
<span v-else class="home-category-icon-placeholder" aria-hidden="true" />
|
||||
<span class="home-category-icon-label">{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -312,14 +307,7 @@ import {
|
||||
clearEventListCache,
|
||||
type EventCardItem,
|
||||
} from '../api/event'
|
||||
import {
|
||||
DEFAULT_CATEGORY_ICON,
|
||||
enrichWithIcons,
|
||||
getPmTagMain,
|
||||
MOCK_CATEGORY_TREE,
|
||||
resolveCategoryIconColor,
|
||||
type CategoryTreeNode,
|
||||
} from '../api/category'
|
||||
import { getPmTagMain, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category'
|
||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
@ -383,10 +371,10 @@ function doSearch(keyword: string) {
|
||||
loadEvents(1, false, keyword)
|
||||
}
|
||||
|
||||
/** 过滤:仅当 forceShow 明确为 false 时不显示,其他(null/undefined/true 等)均显示;排除 forceHide 的节点 */
|
||||
/** 过滤 forceHide 的节点 */
|
||||
function filterVisible(nodes: CategoryTreeNode[] | undefined): CategoryTreeNode[] {
|
||||
if (!nodes?.length) return []
|
||||
return nodes.filter((n) => n.forceShow !== false && !n.forceHide)
|
||||
return nodes.filter((n) => !n.forceHide)
|
||||
}
|
||||
|
||||
/** 第三层区块标题:取当前选中的第一层节点的 sectionTitle 或 label */
|
||||
@ -397,28 +385,6 @@ const selectedLayer0SectionTitle = computed(() => {
|
||||
return node?.sectionTitle ?? node?.label ?? ''
|
||||
})
|
||||
|
||||
/** 从树中递归查找节点 */
|
||||
function findNodeById(nodes: CategoryTreeNode[], id: string): CategoryTreeNode | undefined {
|
||||
for (const n of nodes) {
|
||||
if (n.id === id) return n
|
||||
const found = n.children ? findNodeById(n.children, id) : undefined
|
||||
if (found) return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 当前选中分类的 tag 筛选(取最后选中的层级,用于 API tagId/tagSlug) */
|
||||
const activeTagFilter = computed(() => {
|
||||
const ids = layerActiveValues.value
|
||||
if (ids.length === 0) return { tagId: undefined as number | undefined, tagSlug: undefined as string | undefined }
|
||||
const lastId = ids[ids.length - 1]
|
||||
const root = filterVisible(categoryTree.value)
|
||||
const node = lastId ? findNodeById(root, lastId) : undefined
|
||||
if (!node) return { tagId: undefined, tagSlug: undefined }
|
||||
const tagId = /^\d+$/.test(node.id) ? parseInt(node.id, 10) : undefined
|
||||
return { tagId, tagSlug: node.slug || undefined }
|
||||
})
|
||||
|
||||
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
||||
const categoryLayers = computed(() => {
|
||||
const root = filterVisible(categoryTree.value)
|
||||
@ -439,7 +405,7 @@ const categoryLayers = computed(() => {
|
||||
return layers
|
||||
})
|
||||
|
||||
/** 分类选中时:若有 children 则展开下一层并默认选中第一个,并重新加载列表 */
|
||||
/** 分类选中时:若有 children 则展开下一层并默认选中第一个 */
|
||||
function onCategorySelect(layerIndex: number, selectedId: string) {
|
||||
const nextValues = [...layerActiveValues.value]
|
||||
nextValues[layerIndex] = selectedId
|
||||
@ -455,10 +421,6 @@ function onCategorySelect(layerIndex: number, selectedId: string) {
|
||||
nextValues.push(firstChild.id)
|
||||
}
|
||||
layerActiveValues.value = nextValues
|
||||
|
||||
clearEventListCache()
|
||||
eventPage.value = 1
|
||||
loadEvents(1, false)
|
||||
}
|
||||
|
||||
/** 初始化分类选中:默认选中第一个,若有 children 则递归展开 */
|
||||
@ -560,15 +522,8 @@ const activeSearchKeyword = ref('')
|
||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||
const { tagId, tagSlug } = activeTagFilter.value
|
||||
try {
|
||||
const res = await getPmEventPublic({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
keyword: kw || undefined,
|
||||
tagId,
|
||||
tagSlug,
|
||||
})
|
||||
const res = await getPmEventPublic({ page, pageSize: PAGE_SIZE, keyword: kw || undefined })
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
throw new Error(res.msg || '请求失败')
|
||||
}
|
||||
@ -621,43 +576,37 @@ function checkScrollLoad() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/** 分类树就绪后加载列表(确保 activeTagFilter 已计算,与下拉刷新参数一致) */
|
||||
function loadEventListAfterCategoryReady() {
|
||||
const cached = getEventListCache()
|
||||
if (cached && cached.list.length > 0) {
|
||||
eventList.value = cached.list
|
||||
eventPage.value = cached.page
|
||||
eventTotal.value = cached.total
|
||||
eventPageSize.value = cached.pageSize
|
||||
} else {
|
||||
loadEvents(1, false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */
|
||||
const USE_MOCK_CATEGORY = false
|
||||
if (USE_MOCK_CATEGORY) {
|
||||
categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||
categoryTree.value = MOCK_CATEGORY_TREE
|
||||
initCategorySelection()
|
||||
loadEventListAfterCategoryReady()
|
||||
} else {
|
||||
getPmTagMain()
|
||||
.then((res) => {
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const data = res.data
|
||||
categoryTree.value = Array.isArray(data) && data.length > 0 ? data : enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||
categoryTree.value = Array.isArray(data) && data.length > 0 ? data : MOCK_CATEGORY_TREE
|
||||
} else {
|
||||
categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||
categoryTree.value = MOCK_CATEGORY_TREE
|
||||
}
|
||||
initCategorySelection()
|
||||
loadEventListAfterCategoryReady()
|
||||
})
|
||||
.catch(() => {
|
||||
categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||
categoryTree.value = MOCK_CATEGORY_TREE
|
||||
initCategorySelection()
|
||||
loadEventListAfterCategoryReady()
|
||||
})
|
||||
}
|
||||
|
||||
const cached = getEventListCache()
|
||||
if (cached && cached.list.length > 0) {
|
||||
eventList.value = cached.list
|
||||
eventPage.value = cached.page
|
||||
eventTotal.value = cached.total
|
||||
eventPageSize.value = cached.pageSize
|
||||
} else {
|
||||
loadEvents(1, false)
|
||||
}
|
||||
nextTick(() => {
|
||||
const sentinel = sentinelRef.value
|
||||
if (sentinel) {
|
||||
|
||||
@ -113,78 +113,19 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口;移动端隐藏,改用底部栏+弹窗 -->
|
||||
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
||||
<v-col cols="12" class="trade-col">
|
||||
<div class="trade-sidebar">
|
||||
<TradeComponent ref="tradeComponentRef" :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||
<TradeComponent :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- 移动端:固定底部 Yes/No 栏 + 三点菜单(Merge/Split) -->
|
||||
<template v-if="isMobile && tradeMarketPayload">
|
||||
<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')"
|
||||
>
|
||||
Yes {{ yesPriceCents }}¢
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mobile-bar-btn mobile-bar-no"
|
||||
variant="flat"
|
||||
rounded="sm"
|
||||
@click="openSheetWithOption('no')"
|
||||
>
|
||||
No {{ noPriceCents }}¢
|
||||
</v-btn>
|
||||
<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-icon size="20">mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="openMergeFromBar">
|
||||
<v-list-item-title>Merge</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="openSplitFromBar">
|
||||
<v-list-item-title>Split</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
<v-bottom-sheet v-model="tradeSheetOpen" content-class="trade-detail-trade-sheet">
|
||||
<TradeComponent
|
||||
ref="mobileTradeComponentRef"
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOptionFromBar"
|
||||
embedded-in-sheet
|
||||
/>
|
||||
</v-bottom-sheet>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts } from 'echarts'
|
||||
import OrderBook from '../components/OrderBook.vue'
|
||||
@ -226,8 +167,6 @@ export type ChartIncrement = { point: ChartPoint }
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const { mobile } = useDisplay()
|
||||
const isMobile = computed(() => mobile.value)
|
||||
|
||||
// 详情接口 GET /PmEvent/findPmEvent 返回的数据
|
||||
const eventDetail = ref<PmEventListItem | null>(null)
|
||||
@ -355,45 +294,6 @@ const tradeInitialOption = computed(() => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
/** 移动端底部栏点击 Yes/No 时传给弹窗内 TradeComponent 的初始选项 */
|
||||
const tradeInitialOptionFromBar = ref<'yes' | 'no' | undefined>(undefined)
|
||||
/** 移动端交易弹窗开关 */
|
||||
const tradeSheetOpen = ref(false)
|
||||
/** 移动端三点菜单开关 */
|
||||
const mobileMenuOpen = ref(false)
|
||||
/** 桌面端 TradeComponent 引用(Merge/Split) */
|
||||
const tradeComponentRef = ref<InstanceType<typeof TradeComponent> | null>(null)
|
||||
/** 移动端弹窗内 TradeComponent 引用 */
|
||||
const mobileTradeComponentRef = ref<InstanceType<typeof TradeComponent> | null>(null)
|
||||
|
||||
const yesPriceCents = computed(() =>
|
||||
tradeMarketPayload.value ? Math.round(tradeMarketPayload.value.yesPrice * 100) : 0
|
||||
)
|
||||
const noPriceCents = computed(() =>
|
||||
tradeMarketPayload.value ? Math.round(tradeMarketPayload.value.noPrice * 100) : 0
|
||||
)
|
||||
|
||||
function openSheetWithOption(side: 'yes' | 'no') {
|
||||
tradeInitialOptionFromBar.value = side
|
||||
tradeSheetOpen.value = true
|
||||
}
|
||||
|
||||
function openMergeFromBar() {
|
||||
mobileMenuOpen.value = false
|
||||
tradeSheetOpen.value = true
|
||||
nextTick(() => {
|
||||
mobileTradeComponentRef.value?.openMergeDialog?.()
|
||||
})
|
||||
}
|
||||
|
||||
function openSplitFromBar() {
|
||||
mobileMenuOpen.value = false
|
||||
tradeSheetOpen.value = true
|
||||
nextTick(() => {
|
||||
mobileTradeComponentRef.value?.openSplitDialog?.()
|
||||
})
|
||||
}
|
||||
|
||||
// Comments / Top Holders / Activity
|
||||
const detailTab = ref('activity')
|
||||
const activityMinAmount = ref<string>('0')
|
||||
@ -1330,64 +1230,4 @@ onUnmounted(() => {
|
||||
.activity-link:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 移动端底部交易栏(与 EventMarkets 一致) */
|
||||
.mobile-trade-bar-spacer {
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.mobile-trade-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.mobile-bar-btn {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.mobile-bar-yes {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #b8e0b8 !important;
|
||||
color: #006600 !important;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.mobile-bar-no {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #f0b8b8 !important;
|
||||
color: #cc0000 !important;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.mobile-bar-more-btn {
|
||||
flex-shrink: 0;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
min-width: 46px;
|
||||
min-height: 46px;
|
||||
padding: 0 !important;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb !important;
|
||||
color: #6b7280;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user