快速筛选优化 增加公告
This commit is contained in:
@@ -0,0 +1,244 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import { fetchAnnouncementDetail, type Announcement } from '@/features/announcement'
|
||||||
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const announcement = ref<Announcement | null>(null)
|
||||||
|
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ value: 'notice', label: '通知公告' },
|
||||||
|
{ value: 'tutorial', label: '使用教程' },
|
||||||
|
{ value: 'rule', label: '规则说明' },
|
||||||
|
{ value: 'faq', label: '常见问题' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const renderedContent = computed(() => {
|
||||||
|
if (!announcement.value?.content) return ''
|
||||||
|
return marked.parse(announcement.value.content)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(loadAnnouncement)
|
||||||
|
|
||||||
|
async function loadAnnouncement() {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
if (!id) {
|
||||||
|
router.replace('/m/announcements')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
announcement.value = await fetchAnnouncementDetail(id)
|
||||||
|
} catch {
|
||||||
|
showToast({ message: '公告加载失败', icon: 'cross' })
|
||||||
|
router.replace('/m/announcements')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryLabel(category: string) {
|
||||||
|
return categories.find(item => item.value === category)?.label || '公告'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="mobile-announcement-detail">
|
||||||
|
<header class="page-header">
|
||||||
|
<button class="back-btn" type="button" @click="router.back()">
|
||||||
|
<van-icon name="arrow-left" :size="20" />
|
||||||
|
</button>
|
||||||
|
<h1>公告详情</h1>
|
||||||
|
<span class="header-spacer"></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<van-loading v-if="loading" class="state-loading" size="24px" vertical>
|
||||||
|
正在加载公告...
|
||||||
|
</van-loading>
|
||||||
|
|
||||||
|
<article v-else-if="announcement" class="detail-card">
|
||||||
|
<div class="detail-tags">
|
||||||
|
<span class="category-tag">{{ getCategoryLabel(announcement.category) }}</span>
|
||||||
|
<span v-if="announcement.is_pinned" class="state-tag pinned">置顶</span>
|
||||||
|
<span v-if="announcement.is_important" class="state-tag important">重要</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>{{ announcement.title }}</h2>
|
||||||
|
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span>{{ formatDateTime(announcement.published_at || announcement.created_at) }}</span>
|
||||||
|
<span>浏览 {{ announcement.view_count }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-divider"></div>
|
||||||
|
<div class="detail-content" v-html="renderedContent"></div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-announcement-detail {
|
||||||
|
min-height: 100dvh;
|
||||||
|
background: #f6f8fb;
|
||||||
|
color: #17233d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 44px 1fr 44px;
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: grid;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-loading {
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
margin: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tags,
|
||||||
|
.detail-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag,
|
||||||
|
.state-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-tag.pinned {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-tag.important {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card h2 {
|
||||||
|
margin: 12px 0 10px;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 16px 0;
|
||||||
|
background: #edf0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
color: #334155;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.75;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(p) {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(h1),
|
||||||
|
.detail-content :deep(h2),
|
||||||
|
.detail-content :deep(h3) {
|
||||||
|
margin: 18px 0 10px;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(h1) {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(h2) {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(h3) {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(ul),
|
||||||
|
.detail-content :deep(ol) {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(blockquote) {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-left: 3px solid #ff6a00;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content :deep(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||||
|
import { fetchAnnouncements, type Announcement } from '@/features/announcement'
|
||||||
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const announcements = ref<Announcement[]>([])
|
||||||
|
const activeCategory = ref('')
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 12
|
||||||
|
const total = ref(0)
|
||||||
|
let announcementRequestSeq = 0
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ value: '', label: '全部', icon: 'apps-o' },
|
||||||
|
{ value: 'notice', label: '通知', icon: 'volume-o' },
|
||||||
|
{ value: 'tutorial', label: '教程', icon: 'description' },
|
||||||
|
{ value: 'rule', label: '规则', icon: 'warning-o' },
|
||||||
|
{ value: 'faq', label: '问答', icon: 'question-o' },
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadAnnouncements(reset = false) {
|
||||||
|
if ((loading.value && !reset) || (finished.value && !reset)) return
|
||||||
|
const requestSeq = ++announcementRequestSeq
|
||||||
|
if (reset) {
|
||||||
|
page.value = 1
|
||||||
|
finished.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await fetchAnnouncements({
|
||||||
|
category: activeCategory.value || undefined,
|
||||||
|
page: page.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
})
|
||||||
|
if (requestSeq !== announcementRequestSeq) return
|
||||||
|
announcements.value = reset ? result.items : [...announcements.value, ...result.items]
|
||||||
|
total.value = result.total
|
||||||
|
if (announcements.value.length >= result.total || result.items.length === 0) {
|
||||||
|
finished.value = true
|
||||||
|
} else {
|
||||||
|
page.value += 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (requestSeq !== announcementRequestSeq) return
|
||||||
|
showToast({ message: '公告加载失败,请稍后重试', icon: 'cross' })
|
||||||
|
finished.value = true
|
||||||
|
} finally {
|
||||||
|
if (requestSeq === announcementRequestSeq) {
|
||||||
|
loading.value = false
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRefresh() {
|
||||||
|
refreshing.value = true
|
||||||
|
loadAnnouncements(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
loadAnnouncements(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCategory(category: string) {
|
||||||
|
if (activeCategory.value === category) return
|
||||||
|
activeCategory.value = category
|
||||||
|
onRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(id: number) {
|
||||||
|
router.push(`/m/announcements/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryLabel(category: string) {
|
||||||
|
return categories.find(item => item.value === category)?.label || '公告'
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewText(content: string) {
|
||||||
|
const text = content.replace(/[#>*_`[\]()!-]/g, '').replace(/\s+/g, ' ').trim()
|
||||||
|
return text.length > 76 ? `${text.slice(0, 76)}...` : text
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="mobile-announcements">
|
||||||
|
<header class="page-header">
|
||||||
|
<button class="back-btn" type="button" @click="router.back()">
|
||||||
|
<van-icon name="arrow-left" :size="20" />
|
||||||
|
</button>
|
||||||
|
<h1>公告中心</h1>
|
||||||
|
<span class="header-count">{{ total ? `${total}条` : '' }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<van-pull-refresh v-model="refreshing" class="scroll-container" @refresh="onRefresh">
|
||||||
|
<section class="announcement-content">
|
||||||
|
<div class="announcement-summary">
|
||||||
|
<strong>平台公告</strong>
|
||||||
|
<span>通知、教程、规则和常见问题</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="category-strip" aria-label="公告分类">
|
||||||
|
<button
|
||||||
|
v-for="item in categories"
|
||||||
|
:key="item.value"
|
||||||
|
type="button"
|
||||||
|
class="category-pill"
|
||||||
|
:class="{ active: activeCategory === item.value }"
|
||||||
|
@click="selectCategory(item.value)"
|
||||||
|
>
|
||||||
|
<van-icon :name="item.icon" :size="15" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<van-list
|
||||||
|
v-model:loading="loading"
|
||||||
|
:finished="finished"
|
||||||
|
finished-text="没有更多公告了"
|
||||||
|
:immediate-check="false"
|
||||||
|
@load="onLoad"
|
||||||
|
>
|
||||||
|
<van-empty
|
||||||
|
v-if="!loading && announcements.length === 0"
|
||||||
|
class="empty-state"
|
||||||
|
image="search"
|
||||||
|
description="暂无公告"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="announcement-list">
|
||||||
|
<button
|
||||||
|
v-for="item in announcements"
|
||||||
|
:key="item.id"
|
||||||
|
class="announcement-card"
|
||||||
|
type="button"
|
||||||
|
@click="openDetail(item.id)"
|
||||||
|
>
|
||||||
|
<div class="card-top">
|
||||||
|
<span class="category-tag">{{ getCategoryLabel(item.category) }}</span>
|
||||||
|
<span v-if="item.is_pinned" class="state-tag pinned">置顶</span>
|
||||||
|
<span v-if="item.is_important" class="state-tag important">重要</span>
|
||||||
|
<van-icon class="card-arrow" name="arrow" :size="15" />
|
||||||
|
</div>
|
||||||
|
<h2>{{ item.title }}</h2>
|
||||||
|
<p>{{ previewText(item.content) }}</p>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span>{{ formatDateTime(item.published_at || item.created_at) }}</span>
|
||||||
|
<span>浏览 {{ item.view_count }}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</van-list>
|
||||||
|
</section>
|
||||||
|
</van-pull-refresh>
|
||||||
|
|
||||||
|
<MobileBottomNav />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-announcements {
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding-bottom: calc(62px + env(safe-area-inset-bottom));
|
||||||
|
background: #f6f8fb;
|
||||||
|
color: #17233d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 44px 1fr 72px;
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: grid;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-count {
|
||||||
|
color: #ff6a00;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
min-height: calc(100dvh - 48px - 62px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-summary strong {
|
||||||
|
color: #9a3412;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-summary span {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #c2410c;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-strip {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 -12px;
|
||||||
|
padding: 0 12px 2px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-strip::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-pill.active {
|
||||||
|
border-color: #ff6a00;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-card {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top,
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag,
|
||||||
|
.state-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tag {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-tag.pinned {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-tag.important {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-card h2 {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-card p {
|
||||||
|
display: -webkit-box;
|
||||||
|
min-height: 36px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding-top: 54px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -380,6 +380,7 @@ function resolveAvatarURL(url: string | undefined | null) {
|
|||||||
<div class="menu-card cell-card">
|
<div class="menu-card cell-card">
|
||||||
<van-cell-group :border="false">
|
<van-cell-group :border="false">
|
||||||
<van-cell title="消息中心" icon="chat-o" is-link to="/m/messages" />
|
<van-cell title="消息中心" icon="chat-o" is-link to="/m/messages" />
|
||||||
|
<van-cell title="公告中心" icon="volume-o" is-link to="/m/announcements" />
|
||||||
<van-cell title="资料修改" icon="edit" is-link @click="openProfileEditor" />
|
<van-cell title="资料修改" icon="edit" is-link @click="openProfileEditor" />
|
||||||
<van-cell title="实名认证" icon="idcard" is-link to="/m/realname">
|
<van-cell title="实名认证" icon="idcard" is-link to="/m/realname">
|
||||||
<template #value>
|
<template #value>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
background: #fffdf0;
|
background: #fffdf0;
|
||||||
color: #8a6d1b;
|
color: #8a6d1b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fraud-dot {
|
.fraud-dot {
|
||||||
@@ -120,6 +121,11 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fraud-more {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #c08a12;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-content {
|
.mobile-content {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -257,8 +263,8 @@
|
|||||||
|
|
||||||
.zone-strip {
|
.zone-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
margin: 0 -12px 12px;
|
margin: 0 -12px 10px;
|
||||||
padding: 0 12px 2px;
|
padding: 0 12px 2px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overscroll-behavior-x: contain;
|
overscroll-behavior-x: contain;
|
||||||
@@ -270,55 +276,57 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.zone-pill {
|
.zone-pill {
|
||||||
display: grid;
|
display: inline-flex;
|
||||||
grid-template-columns: auto auto;
|
flex: 0 0 auto;
|
||||||
grid-template-areas:
|
gap: 5px;
|
||||||
'label count'
|
height: 34px;
|
||||||
'hint hint';
|
min-width: 62px;
|
||||||
column-gap: 6px;
|
|
||||||
row-gap: 3px;
|
|
||||||
flex: 0 0 74px;
|
|
||||||
min-height: 54px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 9px;
|
justify-content: center;
|
||||||
border: 1px solid #e2e8f0;
|
padding: 0 11px;
|
||||||
border-radius: 8px;
|
border: 1px solid #dbe3ee;
|
||||||
|
border-radius: 999px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #334155;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-pill strong {
|
.zone-pill strong {
|
||||||
grid-area: label;
|
font-size: 13px;
|
||||||
font-size: 14px;
|
font-weight: 800;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-pill span {
|
.zone-pill span {
|
||||||
grid-area: count;
|
display: grid;
|
||||||
color: #ff6a00;
|
min-width: 17px;
|
||||||
font-size: 13px;
|
height: 17px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
text-align: right;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-pill small {
|
.zone-pill small {
|
||||||
grid-area: hint;
|
display: none;
|
||||||
overflow: hidden;
|
|
||||||
color: #7b8798;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-pill.active {
|
.zone-pill.active {
|
||||||
border-color: #ff6a00;
|
border-color: #ff6a00;
|
||||||
background: #fff7ed;
|
background: #fff7ed;
|
||||||
color: #9a3412;
|
color: #c2410c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-pill.active span {
|
.zone-pill.active span {
|
||||||
color: #ff6a00;
|
background: #ff6a00;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-pill:active {
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-toolbar {
|
.list-toolbar {
|
||||||
@@ -689,6 +697,27 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.back-top-button {
|
||||||
|
position: fixed;
|
||||||
|
right: max(14px, calc((100vw - 430px) / 2 + 14px));
|
||||||
|
bottom: calc(64px + env(safe-area-inset-bottom));
|
||||||
|
z-index: 110;
|
||||||
|
display: grid;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
color: #ff6a00;
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-top-button:active {
|
||||||
|
background: #fff7ed;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
@media (max-width: 360px) {
|
||||||
.mobile-brand small {
|
.mobile-brand small {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -719,4 +748,10 @@
|
|||||||
.card-title-row h2 {
|
.card-title-row h2 {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.back-top-button {
|
||||||
|
right: 12px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const searchValue = ref('')
|
|||||||
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
||||||
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
||||||
const supportLoading = ref(false)
|
const supportLoading = ref(false)
|
||||||
|
const showBackTop = ref(false)
|
||||||
const mobilePageSize = 10
|
const mobilePageSize = 10
|
||||||
let listingRequestSeq = 0
|
let listingRequestSeq = 0
|
||||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -442,10 +443,15 @@ function parseOptionalNumber(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowScroll() {
|
function handleWindowScroll() {
|
||||||
|
showBackTop.value = window.scrollY > 520
|
||||||
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return
|
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return
|
||||||
loadListings(false)
|
loadListings(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
function isSkinGroupKey(key: string) {
|
function isSkinGroupKey(key: string) {
|
||||||
return publishOptions.value.skin_groups.some(group => group.key === key)
|
return publishOptions.value.skin_groups.some(group => group.key === key)
|
||||||
}
|
}
|
||||||
@@ -523,7 +529,7 @@ function chipTone(label: string) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 防骗提示卡片(不用 van-notice-bar) -->
|
<!-- 防骗提示卡片(不用 van-notice-bar) -->
|
||||||
<div class="fraud-tip">
|
<RouterLink class="fraud-tip" to="/m/announcements">
|
||||||
<van-icon name="warning-o" :size="16" color="#b8860b" />
|
<van-icon name="warning-o" :size="16" color="#b8860b" />
|
||||||
<span class="fraud-dot"></span>
|
<span class="fraud-dot"></span>
|
||||||
<van-swipe
|
<van-swipe
|
||||||
@@ -537,7 +543,8 @@ function chipTone(label: string) {
|
|||||||
<span>{{ item }}</span>
|
<span>{{ item }}</span>
|
||||||
</van-swipe-item>
|
</van-swipe-item>
|
||||||
</van-swipe>
|
</van-swipe>
|
||||||
</div>
|
<van-icon class="fraud-more" name="arrow" :size="14" />
|
||||||
|
</RouterLink>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ========== Content 区域 ========== -->
|
<!-- ========== Content 区域 ========== -->
|
||||||
@@ -719,6 +726,16 @@ function chipTone(label: string) {
|
|||||||
:range-presets="rangePresets"
|
:range-presets="rangePresets"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="showBackTop"
|
||||||
|
class="back-top-button"
|
||||||
|
type="button"
|
||||||
|
aria-label="回到顶部"
|
||||||
|
@click="scrollToTop"
|
||||||
|
>
|
||||||
|
<van-icon name="arrow-up" :size="20" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<MobileBottomNav />
|
<MobileBottomNav />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ export const mobileRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/features/chats/views/MobileMessagesView.vue'),
|
component: () => import('@/features/chats/views/MobileMessagesView.vue'),
|
||||||
meta: { layout: 'blank', requiresAuth: true },
|
meta: { layout: 'blank', requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/m/announcements',
|
||||||
|
name: 'mobile-announcements',
|
||||||
|
component: () => import('@/features/announcement/views/MobileAnnouncementsView.vue'),
|
||||||
|
meta: { layout: 'blank' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/m/announcements/:id',
|
||||||
|
name: 'mobile-announcement-detail',
|
||||||
|
component: () => import('@/features/announcement/views/MobileAnnouncementDetailView.vue'),
|
||||||
|
meta: { layout: 'blank' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/m/chats/:id',
|
path: '/m/chats/:id',
|
||||||
name: 'mobile-chat',
|
name: 'mobile-chat',
|
||||||
|
|||||||
Reference in New Issue
Block a user