Compare commits
No commits in common. "f83f0100e0c9ba44298859c97efe00298424b6f5" and "8d103e2d989569f84e40724d34e8c55ae8c367d1" have entirely different histories.
f83f0100e0
...
8d103e2d98
@ -1,110 +0,0 @@
|
|||||||
import { get } from './request'
|
|
||||||
|
|
||||||
/** 分类树节点(与后端返回结构一致) */
|
|
||||||
export interface CategoryTreeNode {
|
|
||||||
id: string
|
|
||||||
label: string
|
|
||||||
slug: string
|
|
||||||
/** 第二层专用:MDI 图标名,如 mdi-view-grid-outline */
|
|
||||||
icon?: string
|
|
||||||
/** 第三层展示时的区块标题,如「加密货币」 */
|
|
||||||
sectionTitle?: string
|
|
||||||
forceShow?: boolean
|
|
||||||
forceHide?: boolean
|
|
||||||
publishedAt?: string
|
|
||||||
updatedBy?: number
|
|
||||||
createdAt?: string
|
|
||||||
updatedAt?: string
|
|
||||||
children?: CategoryTreeNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 模拟分类数据:一层(财经)、二层(体育/加密带图标)、三层(政治) */
|
|
||||||
export const MOCK_CATEGORY_TREE: CategoryTreeNode[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
label: '政治',
|
|
||||||
slug: 'politics',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: '11',
|
|
||||||
label: '政治1',
|
|
||||||
slug: 'politics1',
|
|
||||||
children: [
|
|
||||||
{ id: '111', label: '政治1-A', slug: 'politics1a', children: [] },
|
|
||||||
{ id: '112', label: '政治1-B', slug: 'politics1b', children: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ id: '12', label: '政治2', slug: 'politics2', children: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
label: '体育',
|
|
||||||
slug: 'sports',
|
|
||||||
children: [
|
|
||||||
{ id: '21', label: '足球', slug: 'football', children: [] },
|
|
||||||
{ id: '22', label: '篮球', slug: 'basketball', children: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
label: '加密',
|
|
||||||
slug: 'crypto',
|
|
||||||
sectionTitle: '加密货币',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: '31',
|
|
||||||
label: '全部',
|
|
||||||
slug: 'all',
|
|
||||||
icon: 'mdi-view-grid-outline',
|
|
||||||
children: [
|
|
||||||
{ id: '311', label: '全部', slug: 'all', children: [] },
|
|
||||||
{ id: '312', label: 'Above / Below', slug: 'above-below', children: [] },
|
|
||||||
{ id: '313', label: 'Up / Down', slug: 'up-down', children: [] },
|
|
||||||
{ id: '314', label: 'BTC', slug: 'btc', children: [] },
|
|
||||||
{ id: '315', label: 'ETH', slug: 'eth', children: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ id: '32', label: '5分钟', slug: '5m', icon: 'mdi-format-list-bulleted', children: [] },
|
|
||||||
{ id: '33', label: '15分钟', slug: '15m', icon: 'mdi-clock-outline', children: [] },
|
|
||||||
{ id: '34', label: '每小时', slug: '1h', icon: 'mdi-refresh', children: [] },
|
|
||||||
{ id: '35', label: '4小时', slug: '4h', icon: 'mdi-clock-outline', children: [] },
|
|
||||||
{ id: '36', label: '每天', slug: '1d', icon: 'mdi-calendar', children: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ id: '4', label: '财务', slug: 'finance', children: [] },
|
|
||||||
{ id: '5', label: '地缘政治', slug: 'geopolitics', children: [] },
|
|
||||||
{ id: '0', label: '最新', slug: 'latest', children: [] },
|
|
||||||
]
|
|
||||||
|
|
||||||
/** 分类树接口响应 */
|
|
||||||
export interface CategoryTreeResponse {
|
|
||||||
code: number
|
|
||||||
data: CategoryTreeNode[]
|
|
||||||
msg: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取分类树(公开接口,不需要鉴权)
|
|
||||||
* 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 }
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { ref, readonly } from 'vue'
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'polyclient_search_history'
|
|
||||||
const MAX_HISTORY = 10
|
|
||||||
|
|
||||||
const history = ref<string[]>([])
|
|
||||||
|
|
||||||
function loadHistory() {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
|
||||||
history.value = raw ? JSON.parse(raw) : []
|
|
||||||
} catch {
|
|
||||||
history.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveHistory() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history.value))
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSearchHistory() {
|
|
||||||
const list = readonly(history)
|
|
||||||
|
|
||||||
function add(keyword: string) {
|
|
||||||
const k = keyword.trim()
|
|
||||||
if (!k) return
|
|
||||||
history.value = [k, ...history.value.filter((h) => h !== k)].slice(0, MAX_HISTORY)
|
|
||||||
saveHistory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(index: number) {
|
|
||||||
history.value = history.value.filter((_, i) => i !== index)
|
|
||||||
saveHistory()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAll() {
|
|
||||||
history.value = []
|
|
||||||
saveHistory()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadHistory()
|
|
||||||
|
|
||||||
return { list, add, remove, clearAll }
|
|
||||||
}
|
|
||||||
@ -1,122 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-page">
|
<div class="home-page">
|
||||||
<!-- 第一层:单行紧凑布局,tabs + 搜索/筛选图标 -->
|
|
||||||
<div v-if="categoryLayers.length > 0" class="home-category-layer1-wrap">
|
|
||||||
<div class="home-category-layer1-row">
|
|
||||||
<v-tabs
|
|
||||||
:model-value="layerActiveValues[0]"
|
|
||||||
class="home-tab-bar home-tab-bar--inline"
|
|
||||||
@update:model-value="onCategorySelect(0, $event)"
|
|
||||||
>
|
|
||||||
<v-tab v-for="item in categoryLayers[0]" :key="item.id" :value="item.id">
|
|
||||||
{{ item.label }}
|
|
||||||
</v-tab>
|
|
||||||
</v-tabs>
|
|
||||||
<div class="home-category-layer1-actions">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
variant="text"
|
|
||||||
size="small"
|
|
||||||
class="home-category-action-btn"
|
|
||||||
aria-label="搜索"
|
|
||||||
@click="expandSearch"
|
|
||||||
>
|
|
||||||
<v-icon size="20">mdi-magnify</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn icon variant="text" size="small" class="home-category-action-btn" aria-label="筛选">
|
|
||||||
<v-icon size="20">mdi-filter-outline</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 搜索展开时:浮层输入框 + 历史记录 -->
|
|
||||||
<transition name="home-search-overlay">
|
|
||||||
<div v-if="searchExpanded" class="home-search-overlay">
|
|
||||||
<div class="home-search-overlay-inner">
|
|
||||||
<v-text-field
|
|
||||||
ref="searchInputRef"
|
|
||||||
v-model="searchKeyword"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
placeholder="Search"
|
|
||||||
prepend-inner-icon="mdi-magnify"
|
|
||||||
variant="outlined"
|
|
||||||
class="home-search-overlay-field"
|
|
||||||
@blur="onSearchBlur"
|
|
||||||
@keydown.enter="onSearchSubmit"
|
|
||||||
/>
|
|
||||||
<v-btn icon variant="text" size="small" class="home-search-close-btn" aria-label="收起" @click="collapseSearch">
|
|
||||||
<v-icon size="18">mdi-close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
<div v-if="searchHistoryList.length > 0" class="home-search-history">
|
|
||||||
<div class="home-search-history-header">
|
|
||||||
<span class="home-search-history-title">搜索历史</span>
|
|
||||||
<v-btn
|
|
||||||
variant="text"
|
|
||||||
size="x-small"
|
|
||||||
color="primary"
|
|
||||||
class="home-search-history-clear"
|
|
||||||
@click="searchHistory.clearAll"
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
<ul class="home-search-history-list">
|
|
||||||
<li
|
|
||||||
v-for="(item, idx) in searchHistoryList"
|
|
||||||
:key="`${item}-${idx}`"
|
|
||||||
class="home-search-history-item"
|
|
||||||
>
|
|
||||||
<button type="button" class="home-search-history-text" @click="selectHistoryItem(item)">
|
|
||||||
{{ item }}
|
|
||||||
</button>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
variant="text"
|
|
||||||
size="x-small"
|
|
||||||
class="home-search-history-delete"
|
|
||||||
aria-label="删除"
|
|
||||||
@click.stop="searchHistory.remove(idx)"
|
|
||||||
>
|
|
||||||
<v-icon size="16">mdi-close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
<v-container fluid class="home-container">
|
<v-container fluid class="home-container">
|
||||||
<!-- 第二三层:随内容滚动,回顶部时与列表第一行一起出现 -->
|
<v-row justify="center" align="center" class="home-tabs">
|
||||||
<div v-if="categoryLayers.length >= 2" class="home-category-layers-23-scroll">
|
<v-tabs v-model="activeTab" class="home-tab-bar">
|
||||||
<div class="home-category-layer home-category-layer--icon">
|
<v-tab value="overview">Market Overview</v-tab>
|
||||||
<div class="home-category-icon-row">
|
<v-tab value="trending">Trending</v-tab>
|
||||||
<button
|
<v-tab value="portfolio">Portfolio</v-tab>
|
||||||
v-for="item in categoryLayers[1]"
|
</v-tabs>
|
||||||
:key="item.id"
|
</v-row>
|
||||||
type="button"
|
|
||||||
class="home-category-icon-item"
|
|
||||||
:class="{ 'home-category-icon-item--active': layerActiveValues[1] === item.id }"
|
|
||||||
@click="onCategorySelect(1, item.id)"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div v-if="categoryLayers.length >= 3" class="home-category-layer home-category-layer--third">
|
|
||||||
<v-tabs
|
|
||||||
:model-value="layerActiveValues[2]"
|
|
||||||
class="home-tab-bar home-tab-bar--compact"
|
|
||||||
@update:model-value="onCategorySelect(2, $event)"
|
|
||||||
>
|
|
||||||
<v-tab v-for="item in categoryLayers[2]" :key="item.id" :value="item.id">
|
|
||||||
{{ item.label }}
|
|
||||||
</v-tab>
|
|
||||||
</v-tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 可滚动容器作为 v-pull-to-refresh 的父元素,组件据此判断 scrollTop 仅在顶部时才响应下拉 -->
|
<!-- 可滚动容器作为 v-pull-to-refresh 的父元素,组件据此判断 scrollTop 仅在顶部时才响应下拉 -->
|
||||||
<div ref="scrollRef" class="home-list-scroll">
|
<div ref="scrollRef" class="home-list-scroll">
|
||||||
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
|
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
|
||||||
@ -287,139 +178,11 @@ import {
|
|||||||
clearEventListCache,
|
clearEventListCache,
|
||||||
type EventCardItem,
|
type EventCardItem,
|
||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { getCategoryTree, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category'
|
|
||||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const searchHistory = useSearchHistory()
|
|
||||||
const searchHistoryList = computed(() => searchHistory.list.value)
|
|
||||||
const isMobile = computed(() => mobile.value)
|
const isMobile = computed(() => mobile.value)
|
||||||
|
|
||||||
/** 分类树(顶层) */
|
const activeTab = ref('overview')
|
||||||
const categoryTree = ref<CategoryTreeNode[]>([])
|
|
||||||
/** 每层选中的 id,[layer0, layer1?, layer2?] */
|
|
||||||
const layerActiveValues = ref<string[]>([])
|
|
||||||
/** 第三层搜索框是否展开 */
|
|
||||||
const searchExpanded = ref(false)
|
|
||||||
/** 搜索关键词 */
|
|
||||||
const searchKeyword = ref('')
|
|
||||||
const searchInputRef = ref<{ focus: () => void } | null>(null)
|
|
||||||
|
|
||||||
function expandSearch() {
|
|
||||||
searchExpanded.value = true
|
|
||||||
nextTick(() => {
|
|
||||||
const el = searchInputRef.value as { focus?: () => void } | null
|
|
||||||
el?.focus?.()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function collapseSearch() {
|
|
||||||
searchExpanded.value = false
|
|
||||||
searchKeyword.value = ''
|
|
||||||
if (activeSearchKeyword.value) {
|
|
||||||
activeSearchKeyword.value = ''
|
|
||||||
clearEventListCache()
|
|
||||||
eventPage.value = 1
|
|
||||||
loadEvents(1, false, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSearchBlur() {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!searchKeyword.value.trim()) searchExpanded.value = false
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSearchSubmit() {
|
|
||||||
const k = searchKeyword.value.trim()
|
|
||||||
if (k) {
|
|
||||||
searchHistory.add(k)
|
|
||||||
doSearch(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectHistoryItem(item: string) {
|
|
||||||
searchKeyword.value = item
|
|
||||||
searchHistory.add(item)
|
|
||||||
doSearch(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch(keyword: string) {
|
|
||||||
activeSearchKeyword.value = keyword
|
|
||||||
clearEventListCache()
|
|
||||||
eventPage.value = 1
|
|
||||||
loadEvents(1, false, keyword)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 过滤 forceHide 的节点 */
|
|
||||||
function filterVisible(nodes: CategoryTreeNode[] | undefined): CategoryTreeNode[] {
|
|
||||||
if (!nodes?.length) return []
|
|
||||||
return nodes.filter((n) => !n.forceHide)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 第三层区块标题:取当前选中的第一层节点的 sectionTitle 或 label */
|
|
||||||
const selectedLayer0SectionTitle = computed(() => {
|
|
||||||
const root = filterVisible(categoryTree.value)
|
|
||||||
const id = layerActiveValues.value[0]
|
|
||||||
const node = id ? root.find((n) => n.id === id) : root[0]
|
|
||||||
return node?.sectionTitle ?? node?.label ?? ''
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
|
||||||
const categoryLayers = computed(() => {
|
|
||||||
const root = filterVisible(categoryTree.value)
|
|
||||||
if (root.length === 0) return []
|
|
||||||
|
|
||||||
const layers: CategoryTreeNode[][] = [root]
|
|
||||||
const active = layerActiveValues.value
|
|
||||||
|
|
||||||
let currentNodes = root
|
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
const selectedId = active[i]
|
|
||||||
const node = selectedId ? currentNodes.find((n) => n.id === selectedId) : currentNodes[0]
|
|
||||||
const children = filterVisible(node?.children)
|
|
||||||
if (children.length === 0) break
|
|
||||||
layers.push(children)
|
|
||||||
currentNodes = children
|
|
||||||
}
|
|
||||||
return layers
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 分类选中时:若有 children 则展开下一层并默认选中第一个 */
|
|
||||||
function onCategorySelect(layerIndex: number, selectedId: string) {
|
|
||||||
const nextValues = [...layerActiveValues.value]
|
|
||||||
nextValues[layerIndex] = selectedId
|
|
||||||
nextValues.length = layerIndex + 1
|
|
||||||
|
|
||||||
const layers = categoryLayers.value
|
|
||||||
const layer = layers[layerIndex]
|
|
||||||
const node = layer?.find((n) => n.id === selectedId)
|
|
||||||
const children = filterVisible(node?.children)
|
|
||||||
|
|
||||||
const firstChild = children[0]
|
|
||||||
if (firstChild && layerIndex < 2) {
|
|
||||||
nextValues.push(firstChild.id)
|
|
||||||
}
|
|
||||||
layerActiveValues.value = nextValues
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化分类选中:默认选中第一个,若有 children 则递归展开 */
|
|
||||||
function initCategorySelection() {
|
|
||||||
const root = filterVisible(categoryTree.value)
|
|
||||||
if (root.length === 0) return
|
|
||||||
|
|
||||||
const values: string[] = []
|
|
||||||
let current = root
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const first = current[0]
|
|
||||||
if (!first) break
|
|
||||||
values.push(first.id)
|
|
||||||
const children = filterVisible(first.children)
|
|
||||||
if (children.length === 0) break
|
|
||||||
current = children
|
|
||||||
}
|
|
||||||
layerActiveValues.value = values
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
@ -485,15 +248,11 @@ function updateGridColumns() {
|
|||||||
gridColumns.value = Math.max(1, n)
|
gridColumns.value = Math.max(1, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前生效的搜索关键词(用于分页加载) */
|
|
||||||
const activeSearchKeyword = ref('')
|
|
||||||
|
|
||||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
async function loadEvents(page: number, append: boolean) {
|
||||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
|
||||||
try {
|
try {
|
||||||
const res = await getPmEventPublic(
|
const res = await getPmEventPublic(
|
||||||
{ page, pageSize: PAGE_SIZE, keyword: kw || undefined }
|
{ page, pageSize: PAGE_SIZE }
|
||||||
)
|
)
|
||||||
if (res.code !== 0 && res.code !== 200) {
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
throw new Error(res.msg || '请求失败')
|
throw new Error(res.msg || '请求失败')
|
||||||
@ -527,7 +286,7 @@ async function loadEvents(page: number, append: boolean, keyword?: string) {
|
|||||||
function onRefresh({ done }: { done: () => void }) {
|
function onRefresh({ done }: { done: () => void }) {
|
||||||
clearEventListCache()
|
clearEventListCache()
|
||||||
eventPage.value = 1
|
eventPage.value = 1
|
||||||
loadEvents(1, false, activeSearchKeyword.value).finally(() => done())
|
loadEvents(1, false).finally(() => done())
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
@ -547,28 +306,6 @@ function checkScrollLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
/** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */
|
|
||||||
const USE_MOCK_CATEGORY = true
|
|
||||||
if (USE_MOCK_CATEGORY) {
|
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
|
||||||
initCategorySelection()
|
|
||||||
} else {
|
|
||||||
getCategoryTree()
|
|
||||||
.then((res) => {
|
|
||||||
if (res.code === 0 || res.code === 200) {
|
|
||||||
const data = res.data
|
|
||||||
categoryTree.value = Array.isArray(data) && data.length > 0 ? data : MOCK_CATEGORY_TREE
|
|
||||||
} else {
|
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
|
||||||
}
|
|
||||||
initCategorySelection()
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
|
||||||
initCategorySelection()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const cached = getEventListCache()
|
const cached = getEventListCache()
|
||||||
if (cached && cached.list.length > 0) {
|
if (cached && cached.list.length > 0) {
|
||||||
eventList.value = cached.list
|
eventList.value = cached.list
|
||||||
@ -730,261 +467,16 @@ onUnmounted(() => {
|
|||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 第一层:置顶、全宽 */
|
|
||||||
.home-category-layer1-wrap {
|
|
||||||
width: 100vw;
|
|
||||||
max-width: 100vw;
|
|
||||||
margin-left: calc(50% - 50vw);
|
|
||||||
position: sticky;
|
|
||||||
top: 64px;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer1-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-tab-bar--inline {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-tab-bar--inline :deep(.v-tabs) {
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-tab-bar--inline :deep(.v-tab) {
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer1-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 搜索浮层:绝对定位,遮挡第二三层,不顶开布局 */
|
|
||||||
.home-search-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 11;
|
|
||||||
padding: 8px 12px 12px;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-overlay-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-overlay-field {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-close-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-overlay-field :deep(.v-field) {
|
|
||||||
background-color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-overlay-enter-active,
|
|
||||||
.home-search-overlay-leave-active {
|
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-overlay-enter-from,
|
|
||||||
.home-search-overlay-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding-top: 8px;
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-title {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-clear {
|
|
||||||
min-width: auto;
|
|
||||||
padding: 0 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
max-height: 160px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 0;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-text {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #1e293b;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-text:hover {
|
|
||||||
color: rgb(var(--v-theme-primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-delete {
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 28px;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search-history-delete:hover {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 第二三层:随内容滚动,全宽 */
|
|
||||||
.home-category-layers-23-scroll {
|
|
||||||
width: 100vw;
|
|
||||||
max-width: 100vw;
|
|
||||||
margin-left: calc(50% - 50vw);
|
|
||||||
margin-bottom: 0;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer--text :deep(.v-tabs) {
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer--icon {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 8px;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-row::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-item {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 56px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: transparent;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-item:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.04);
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-item--active {
|
|
||||||
background-color: rgb(var(--v-theme-primary));
|
|
||||||
color: rgb(var(--v-theme-on-primary));
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-placeholder {
|
|
||||||
display: block;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-icon-label {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-layer--third {
|
|
||||||
padding: 12px 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-category-action-btn {
|
|
||||||
min-width: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-tab-bar {
|
.home-tab-bar {
|
||||||
position: relative;
|
position: fixed;
|
||||||
top: auto;
|
top: 64px; /* Adjust based on app bar height */
|
||||||
left: auto;
|
left: 0;
|
||||||
transform: none;
|
transform: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: transparent;
|
z-index: 10;
|
||||||
margin-bottom: 0;
|
background-color: white;
|
||||||
box-shadow: none;
|
margin-bottom: 20px;
|
||||||
}
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
.home-tab-bar--compact :deep(.v-tab) {
|
|
||||||
min-height: 40px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-card {
|
.home-card {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user