优化:UI调整
This commit is contained in:
parent
89ba32bdbc
commit
2174abc9d3
@ -1,4 +1,11 @@
|
|||||||
import { get } from './request'
|
import { get } from './request'
|
||||||
|
import {
|
||||||
|
DEFAULT_CATEGORY_ICON,
|
||||||
|
resolveCategoryIcon,
|
||||||
|
resolveCategoryIconColor,
|
||||||
|
} from './categoryIcons'
|
||||||
|
|
||||||
|
export { DEFAULT_CATEGORY_ICON, resolveCategoryIconColor }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接口返回的 PmTag 结构(definitions polymarket.PmTag)
|
* 接口返回的 PmTag 结构(definitions polymarket.PmTag)
|
||||||
@ -22,6 +29,8 @@ export interface PmTagMainItem {
|
|||||||
icon?: string
|
icon?: string
|
||||||
sectionTitle?: string
|
sectionTitle?: string
|
||||||
forceHide?: boolean
|
forceHide?: boolean
|
||||||
|
/** 排序值,有则按从小到大排序 */
|
||||||
|
sort?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分类树节点(与后端返回结构一致) */
|
/** 分类树节点(与后端返回结构一致) */
|
||||||
@ -39,6 +48,8 @@ export interface CategoryTreeNode {
|
|||||||
updatedBy?: number
|
updatedBy?: number
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
|
/** 排序值,有则按从小到大排序 */
|
||||||
|
sort?: number
|
||||||
children?: CategoryTreeNode[]
|
children?: CategoryTreeNode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,58 +119,89 @@ export interface CategoryTreeResponse {
|
|||||||
msg: string
|
msg: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 递归为树节点补充图标(无 icon 时自动匹配),可导出供 mock 等场景使用 */
|
||||||
* 获取分类树(公开接口,不需要鉴权)
|
export function enrichWithIcons(nodes: CategoryTreeNode[]): CategoryTreeNode[] {
|
||||||
* GET /PmTag/getPmTagPublic
|
return nodes.map((n) => {
|
||||||
*
|
const icon = n.icon ?? resolveCategoryIcon(n)
|
||||||
* 返回带 children 的树形结构,最多三层
|
const children = n.children?.length ? enrichWithIcons(n.children) : undefined
|
||||||
* data 可能为数组,或 { list: [] } 等格式,统一转为 CategoryTreeNode[]
|
return { ...n, icon: icon ?? n.icon, children }
|
||||||
*/
|
})
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将 PmTagMainItem 转为 CategoryTreeNode */
|
/** 递归按 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 图标 */
|
||||||
function mapPmTagToTreeNode(item: PmTagMainItem): CategoryTreeNode {
|
function mapPmTagToTreeNode(item: PmTagMainItem): CategoryTreeNode {
|
||||||
const rawId = item.ID
|
const rawId = item.ID
|
||||||
const id = rawId != null ? String(rawId) : (item.slug ?? item.label ?? '')
|
const id = rawId != null ? String(rawId) : (item.slug ?? item.label ?? '')
|
||||||
const children = Array.isArray(item.children) ? item.children.map(mapPmTagToTreeNode) : undefined
|
const children = Array.isArray(item.children) ? item.children.map(mapPmTagToTreeNode) : undefined
|
||||||
|
const icon = item.icon ?? resolveCategoryIcon({ label: item.label, slug: item.slug })
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: item.label ?? '',
|
label: item.label ?? '',
|
||||||
slug: item.slug ?? '',
|
slug: item.slug ?? '',
|
||||||
icon: item.icon,
|
icon,
|
||||||
sectionTitle: item.sectionTitle,
|
sectionTitle: item.sectionTitle,
|
||||||
forceShow: item.forceShow,
|
forceShow: item.forceShow,
|
||||||
forceHide: item.forceHide,
|
forceHide: item.forceHide,
|
||||||
|
sort: item.sort,
|
||||||
children: children?.length ? children : undefined,
|
children: children?.length ? children : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** getPmTagList 响应:data 可能为 { All: PmTag[] } 或 PmTag[] */
|
||||||
|
type GetPmTagListData = PmTagMainItem[] | { All?: PmTagMainItem[] }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主分类(首页顶部分类 Tab 数据)
|
* 获取主分类(首页顶部分类 Tab 数据)
|
||||||
* GET /PmTag/getPmTagMain
|
* GET /PmTag/getPmTagList(原 getPmTagMain)
|
||||||
*
|
*
|
||||||
* 不需要鉴权,返回带 children 的树形结构,最多三层
|
* 数据结构:三层结构均在 data.All 下,data.All 为根节点数组
|
||||||
*/
|
*/
|
||||||
export async function getPmTagMain(): Promise<CategoryTreeResponse> {
|
export async function getPmTagMain(): Promise<CategoryTreeResponse> {
|
||||||
const res = await get<{ code: number; data: PmTagMainItem[]; msg: string }>('/PmTag/getPmTagMain')
|
const res = await get<{ code: number; data: GetPmTagListData; msg: string }>(
|
||||||
const data: CategoryTreeNode[] = Array.isArray(res.data) ? res.data.map(mapPmTagToTreeNode) : []
|
'/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))
|
||||||
return { code: res.code, data, msg: res.msg }
|
return { code: res.code, data, msg: res.msg }
|
||||||
}
|
}
|
||||||
|
|||||||
212
src/api/categoryIcons.ts
Normal file
212
src/api/categoryIcons.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* 第二层分类图标自动匹配
|
||||||
|
* 基于 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,19 +121,22 @@ export interface GetPmEventListParams {
|
|||||||
createdAtRange?: string[]
|
createdAtRange?: string[]
|
||||||
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
|
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
|
||||||
tokenid?: string | string[]
|
tokenid?: string | string[]
|
||||||
|
/** 标签 ID,按分类筛选 */
|
||||||
|
tagId?: number
|
||||||
|
/** 标签 slug,按分类筛选 */
|
||||||
|
tagSlug?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||||||
* GET /PmEvent/getPmEventPublic
|
* GET /PmEvent/getPmEventPublic
|
||||||
*
|
*
|
||||||
* Query: page, pageSize, keyword, createdAtRange, tokenid
|
* Query: page, pageSize, keyword, createdAtRange, tokenid, tagId, tagSlug
|
||||||
* tokenid 对应 market.clobTokenIds 中的值,可传单个或数组
|
|
||||||
*/
|
*/
|
||||||
export async function getPmEventPublic(
|
export async function getPmEventPublic(
|
||||||
params: GetPmEventListParams = {},
|
params: GetPmEventListParams = {},
|
||||||
): Promise<PmEventListResponse> {
|
): Promise<PmEventListResponse> {
|
||||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params
|
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid, tagId, tagSlug } = params
|
||||||
const query: Record<string, string | number | string[] | undefined> = {
|
const query: Record<string, string | number | string[] | undefined> = {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -143,6 +146,8 @@ export async function getPmEventPublic(
|
|||||||
if (tokenid != null) {
|
if (tokenid != null) {
|
||||||
query.tokenid = Array.isArray(tokenid) ? tokenid : [tokenid]
|
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)
|
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1067,6 +1067,8 @@ async function submitSplit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({ openMergeDialog, openSplitDialog })
|
||||||
|
|
||||||
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
|
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
|
||||||
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
|
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
|
||||||
|
|
||||||
|
|||||||
@ -83,17 +83,19 @@
|
|||||||
class="buy-yes-btn"
|
class="buy-yes-btn"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
size="small"
|
size="small"
|
||||||
|
rounded="sm"
|
||||||
@click="openTrade(market, index, 'yes')"
|
@click="openTrade(market, index, 'yes')"
|
||||||
>
|
>
|
||||||
Buy Yes {{ yesPrice(market) }}
|
Yes {{ yesPrice(market) }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="buy-no-btn"
|
class="buy-no-btn"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
size="small"
|
size="small"
|
||||||
|
rounded="sm"
|
||||||
@click="openTrade(market, index, 'no')"
|
@click="openTrade(market, index, 'no')"
|
||||||
>
|
>
|
||||||
Buy No {{ noPrice(market) }}
|
No {{ noPrice(market) }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,36 +115,60 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- 移动端:单 market 时显示固定底部 Buy Yes/No 栏;多 market 时仅通过列表点击弹出 -->
|
<!-- 移动端:单 market 时显示固定底部 Yes/No 栏 + 三点菜单(Merge/Split) -->
|
||||||
<template v-if="isMobile && markets.length === 1">
|
<template v-if="isMobile && markets.length === 1">
|
||||||
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
|
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
|
||||||
<div class="mobile-trade-bar">
|
<div class="mobile-trade-bar">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mobile-bar-btn mobile-bar-yes"
|
class="mobile-bar-btn mobile-bar-yes"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
color="success"
|
rounded="sm"
|
||||||
rounded="pill"
|
|
||||||
block
|
|
||||||
@click="openSheetWithOption('yes')"
|
@click="openSheetWithOption('yes')"
|
||||||
>
|
>
|
||||||
Buy Yes {{ barMarket ? yesPrice(barMarket) : '0¢' }}
|
Yes {{ barMarket ? yesPrice(barMarket) : '0¢' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mobile-bar-btn mobile-bar-no"
|
class="mobile-bar-btn mobile-bar-no"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
color="error"
|
rounded="sm"
|
||||||
rounded="pill"
|
|
||||||
block
|
|
||||||
@click="openSheetWithOption('no')"
|
@click="openSheetWithOption('no')"
|
||||||
>
|
>
|
||||||
Buy No {{ barMarket ? noPrice(barMarket) : '0¢' }}
|
No {{ barMarket ? noPrice(barMarket) : '0¢' }}
|
||||||
</v-btn>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 移动端交易弹窗:单 market 与多 market 均需(多 market 时通过列表 Buy Yes/No 打开) -->
|
<!-- 移动端交易弹窗:单 market 与多 market 均需(多 market 时通过列表 Buy Yes/No 打开) -->
|
||||||
<template v-if="isMobile && markets.length > 0">
|
<template v-if="isMobile && markets.length > 0">
|
||||||
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
|
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
|
ref="tradeComponentRef"
|
||||||
:key="`trade-${selectedMarketIndex}-${tradeInitialOption}`"
|
:key="`trade-${selectedMarketIndex}-${tradeInitialOption}`"
|
||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
:initial-option="tradeInitialOption"
|
:initial-option="tradeInitialOption"
|
||||||
@ -188,6 +214,10 @@ const selectedMarketIndex = ref(0)
|
|||||||
const tradeInitialOption = ref<'yes' | 'no' | undefined>(undefined)
|
const tradeInitialOption = ref<'yes' | 'no' | undefined>(undefined)
|
||||||
/** 移动端交易弹窗开关 */
|
/** 移动端交易弹窗开关 */
|
||||||
const tradeSheetOpen = ref(false)
|
const tradeSheetOpen = ref(false)
|
||||||
|
/** 移动端底部栏三点菜单开关 */
|
||||||
|
const mobileMenuOpen = ref(false)
|
||||||
|
/** TradeComponent 引用,用于从底部栏触发 Merge/Split */
|
||||||
|
const tradeComponentRef = ref<InstanceType<typeof TradeComponent> | null>(null)
|
||||||
const markets = computed(() => {
|
const markets = computed(() => {
|
||||||
const list = eventDetail.value?.markets ?? []
|
const list = eventDetail.value?.markets ?? []
|
||||||
return list.length > 0 ? list : []
|
return list.length > 0 ? list : []
|
||||||
@ -491,12 +521,30 @@ function openTrade(market: PmEventMarketItem, index: number, side: 'yes' | 'no')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 移动端底部栏:点击 Buy Yes/No 时打开交易弹窗 */
|
/** 移动端底部栏:点击 Yes/No 时打开交易弹窗 */
|
||||||
function openSheetWithOption(side: 'yes' | 'no') {
|
function openSheetWithOption(side: 'yes' | 'no') {
|
||||||
tradeInitialOption.value = side
|
tradeInitialOption.value = side
|
||||||
tradeSheetOpen.value = true
|
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: {
|
function onTradeSubmit(payload: {
|
||||||
side: 'buy' | 'sell'
|
side: 'buy' | 'sell'
|
||||||
option: 'yes' | 'no'
|
option: 'yes' | 'no'
|
||||||
@ -898,22 +946,39 @@ watch(
|
|||||||
|
|
||||||
.market-row-actions {
|
.market-row-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
flex-shrink: 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy-yes-btn {
|
.buy-yes-btn {
|
||||||
background-color: #b8e0b8 !important;
|
background-color: #b8e0b8 !important;
|
||||||
color: #006600 !important;
|
color: #006600 !important;
|
||||||
text-transform: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy-no-btn {
|
.buy-no-btn {
|
||||||
background-color: #f0b8b8 !important;
|
background-color: #f0b8b8 !important;
|
||||||
color: #cc0000 !important;
|
color: #cc0000 !important;
|
||||||
text-transform: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端底部交易栏 */
|
/* 移动端底部交易栏 */
|
||||||
@ -929,7 +994,7 @@ watch(
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||||
@ -939,9 +1004,9 @@ watch(
|
|||||||
|
|
||||||
.mobile-bar-btn {
|
.mobile-bar-btn {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 9999px;
|
border-radius: 6px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
@ -950,16 +1015,29 @@ watch(
|
|||||||
.mobile-bar-yes {
|
.mobile-bar-yes {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background: #16a34a !important;
|
background: #b8e0b8 !important;
|
||||||
color: #fff !important;
|
color: #006600 !important;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-bar-no {
|
.mobile-bar-no {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background: #dc2626 !important;
|
background: #f0b8b8 !important;
|
||||||
color: #fff !important;
|
color: #cc0000 !important;
|
||||||
padding: 14px 16px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -116,8 +116,13 @@
|
|||||||
:class="{ 'home-category-icon-item--active': layerActiveValues[1] === item.id }"
|
:class="{ 'home-category-icon-item--active': layerActiveValues[1] === item.id }"
|
||||||
@click="onCategorySelect(1, item.id)"
|
@click="onCategorySelect(1, item.id)"
|
||||||
>
|
>
|
||||||
<v-icon v-if="item.icon" size="24" class="home-category-icon">{{ item.icon }}</v-icon>
|
<v-icon
|
||||||
<span v-else class="home-category-icon-placeholder" aria-hidden="true" />
|
size="24"
|
||||||
|
class="home-category-icon"
|
||||||
|
:color="resolveCategoryIconColor(item) ?? 'grey'"
|
||||||
|
>
|
||||||
|
{{ item.icon || DEFAULT_CATEGORY_ICON }}
|
||||||
|
</v-icon>
|
||||||
<span class="home-category-icon-label">{{ item.label }}</span>
|
<span class="home-category-icon-label">{{ item.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -307,7 +312,14 @@ import {
|
|||||||
clearEventListCache,
|
clearEventListCache,
|
||||||
type EventCardItem,
|
type EventCardItem,
|
||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { getPmTagMain, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category'
|
import {
|
||||||
|
DEFAULT_CATEGORY_ICON,
|
||||||
|
enrichWithIcons,
|
||||||
|
getPmTagMain,
|
||||||
|
MOCK_CATEGORY_TREE,
|
||||||
|
resolveCategoryIconColor,
|
||||||
|
type CategoryTreeNode,
|
||||||
|
} from '../api/category'
|
||||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
import { useSearchHistory } from '../composables/useSearchHistory'
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
@ -371,10 +383,10 @@ function doSearch(keyword: string) {
|
|||||||
loadEvents(1, false, keyword)
|
loadEvents(1, false, keyword)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 过滤 forceHide 的节点 */
|
/** 过滤:仅当 forceShow 明确为 false 时不显示,其他(null/undefined/true 等)均显示;排除 forceHide 的节点 */
|
||||||
function filterVisible(nodes: CategoryTreeNode[] | undefined): CategoryTreeNode[] {
|
function filterVisible(nodes: CategoryTreeNode[] | undefined): CategoryTreeNode[] {
|
||||||
if (!nodes?.length) return []
|
if (!nodes?.length) return []
|
||||||
return nodes.filter((n) => !n.forceHide)
|
return nodes.filter((n) => n.forceShow !== false && !n.forceHide)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 第三层区块标题:取当前选中的第一层节点的 sectionTitle 或 label */
|
/** 第三层区块标题:取当前选中的第一层节点的 sectionTitle 或 label */
|
||||||
@ -385,6 +397,28 @@ const selectedLayer0SectionTitle = computed(() => {
|
|||||||
return node?.sectionTitle ?? node?.label ?? ''
|
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]?] */
|
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
||||||
const categoryLayers = computed(() => {
|
const categoryLayers = computed(() => {
|
||||||
const root = filterVisible(categoryTree.value)
|
const root = filterVisible(categoryTree.value)
|
||||||
@ -405,7 +439,7 @@ const categoryLayers = computed(() => {
|
|||||||
return layers
|
return layers
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 分类选中时:若有 children 则展开下一层并默认选中第一个 */
|
/** 分类选中时:若有 children 则展开下一层并默认选中第一个,并重新加载列表 */
|
||||||
function onCategorySelect(layerIndex: number, selectedId: string) {
|
function onCategorySelect(layerIndex: number, selectedId: string) {
|
||||||
const nextValues = [...layerActiveValues.value]
|
const nextValues = [...layerActiveValues.value]
|
||||||
nextValues[layerIndex] = selectedId
|
nextValues[layerIndex] = selectedId
|
||||||
@ -421,6 +455,10 @@ function onCategorySelect(layerIndex: number, selectedId: string) {
|
|||||||
nextValues.push(firstChild.id)
|
nextValues.push(firstChild.id)
|
||||||
}
|
}
|
||||||
layerActiveValues.value = nextValues
|
layerActiveValues.value = nextValues
|
||||||
|
|
||||||
|
clearEventListCache()
|
||||||
|
eventPage.value = 1
|
||||||
|
loadEvents(1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化分类选中:默认选中第一个,若有 children 则递归展开 */
|
/** 初始化分类选中:默认选中第一个,若有 children 则递归展开 */
|
||||||
@ -522,8 +560,15 @@ const activeSearchKeyword = ref('')
|
|||||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||||
|
const { tagId, tagSlug } = activeTagFilter.value
|
||||||
try {
|
try {
|
||||||
const res = await getPmEventPublic({ page, pageSize: PAGE_SIZE, keyword: kw || undefined })
|
const res = await getPmEventPublic({
|
||||||
|
page,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
keyword: kw || undefined,
|
||||||
|
tagId,
|
||||||
|
tagSlug,
|
||||||
|
})
|
||||||
if (res.code !== 0 && res.code !== 200) {
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
throw new Error(res.msg || '请求失败')
|
throw new Error(res.msg || '请求失败')
|
||||||
}
|
}
|
||||||
@ -576,37 +621,43 @@ function checkScrollLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
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 */
|
/** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */
|
||||||
const USE_MOCK_CATEGORY = false
|
const USE_MOCK_CATEGORY = false
|
||||||
if (USE_MOCK_CATEGORY) {
|
if (USE_MOCK_CATEGORY) {
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||||
initCategorySelection()
|
initCategorySelection()
|
||||||
|
loadEventListAfterCategoryReady()
|
||||||
} else {
|
} else {
|
||||||
getPmTagMain()
|
getPmTagMain()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
const data = res.data
|
const data = res.data
|
||||||
categoryTree.value = Array.isArray(data) && data.length > 0 ? data : MOCK_CATEGORY_TREE
|
categoryTree.value = Array.isArray(data) && data.length > 0 ? data : enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||||
} else {
|
} else {
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||||
}
|
}
|
||||||
initCategorySelection()
|
initCategorySelection()
|
||||||
|
loadEventListAfterCategoryReady()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
categoryTree.value = enrichWithIcons(MOCK_CATEGORY_TREE)
|
||||||
initCategorySelection()
|
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(() => {
|
nextTick(() => {
|
||||||
const sentinel = sentinelRef.value
|
const sentinel = sentinelRef.value
|
||||||
if (sentinel) {
|
if (sentinel) {
|
||||||
|
|||||||
@ -113,19 +113,78 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口;移动端隐藏,改用底部栏+弹窗 -->
|
||||||
<v-col cols="12" class="trade-col">
|
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
||||||
<div class="trade-sidebar">
|
<div class="trade-sidebar">
|
||||||
<TradeComponent :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
<TradeComponent ref="tradeComponentRef" :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</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-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import type { ECharts } from 'echarts'
|
import type { ECharts } from 'echarts'
|
||||||
import OrderBook from '../components/OrderBook.vue'
|
import OrderBook from '../components/OrderBook.vue'
|
||||||
@ -167,6 +226,8 @@ export type ChartIncrement = { point: ChartPoint }
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { mobile } = useDisplay()
|
||||||
|
const isMobile = computed(() => mobile.value)
|
||||||
|
|
||||||
// 详情接口 GET /PmEvent/findPmEvent 返回的数据
|
// 详情接口 GET /PmEvent/findPmEvent 返回的数据
|
||||||
const eventDetail = ref<PmEventListItem | null>(null)
|
const eventDetail = ref<PmEventListItem | null>(null)
|
||||||
@ -294,6 +355,45 @@ const tradeInitialOption = computed(() => {
|
|||||||
return undefined
|
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
|
// Comments / Top Holders / Activity
|
||||||
const detailTab = ref('activity')
|
const detailTab = ref('activity')
|
||||||
const activityMinAmount = ref<string>('0')
|
const activityMinAmount = ref<string>('0')
|
||||||
@ -1230,4 +1330,64 @@ onUnmounted(() => {
|
|||||||
.activity-link:hover {
|
.activity-link:hover {
|
||||||
color: #6b7280;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user