优化首页返回定位与发布在线时间表单
移动端首页用列表缓存和内部滚动恢复浏览位置;发布页在线时间改为全天+起止时间单行布局。
This commit is contained in:
@@ -8,9 +8,6 @@ import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
|
|||||||
import AdminLayout from './layouts/AdminLayout.vue'
|
import AdminLayout from './layouts/AdminLayout.vue'
|
||||||
import AppLayout from './layouts/AppLayout.vue'
|
import AppLayout from './layouts/AppLayout.vue'
|
||||||
|
|
||||||
/** 移动端从详情返回时保留列表状态与滚动位置 */
|
|
||||||
const mobileKeepAliveIncludes = ['MobileHomeView']
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const layout = computed(() => {
|
const layout = computed(() => {
|
||||||
if (route.path === '/m' || route.path.startsWith('/m/')) {
|
if (route.path === '/m' || route.path.startsWith('/m/')) {
|
||||||
@@ -26,11 +23,7 @@ const layout = computed(() => {
|
|||||||
<AdminLayout v-if="layout === 'admin'">
|
<AdminLayout v-if="layout === 'admin'">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
<RouterView v-else-if="layout === 'blank'" v-slot="{ Component }">
|
<RouterView v-else-if="layout === 'blank'" />
|
||||||
<KeepAlive :include="mobileKeepAliveIncludes" :max="5">
|
|
||||||
<component :is="Component" />
|
|
||||||
</KeepAlive>
|
|
||||||
</RouterView>
|
|
||||||
<AppLayout v-else>
|
<AppLayout v-else>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
.mobile-shell {
|
.mobile-shell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 430px;
|
max-width: 430px;
|
||||||
min-height: 100vh;
|
height: 100dvh;
|
||||||
|
height: 100vh;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
background: #f6f8fb;
|
background: #f6f8fb;
|
||||||
color: #17233d;
|
color: #17233d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 列表独立滚动:与详情页 window 滚动隔离,返回时恢复 scrollTop */
|
||||||
|
.mobile-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-hero {
|
.mobile-hero {
|
||||||
padding: 12px 12px 10px;
|
padding: 12px 12px 10px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
computed,
|
import { onBeforeRouteLeave, RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
nextTick,
|
|
||||||
onActivated,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onDeactivated,
|
|
||||||
onMounted,
|
|
||||||
ref,
|
|
||||||
watch,
|
|
||||||
} from 'vue'
|
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||||
import { ensureSupportChat } from '@/features/chats/api/chats'
|
import { ensureSupportChat } from '@/features/chats/api/chats'
|
||||||
@@ -58,13 +49,17 @@ import {
|
|||||||
hasAcceleratedSaleRatio,
|
hasAcceleratedSaleRatio,
|
||||||
hasGiftResources,
|
hasGiftResources,
|
||||||
} from '@/shared/utils/listingDisplay'
|
} from '@/shared/utils/listingDisplay'
|
||||||
|
import {
|
||||||
|
buildMobileHomeListSignature,
|
||||||
|
useMobileHomeCacheStore,
|
||||||
|
} from '@/stores/mobileHomeCache'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
|
||||||
defineOptions({ name: 'MobileHomeView' })
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
|
const homeCache = useMobileHomeCacheStore()
|
||||||
|
const scrollEl = ref<HTMLElement | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const loadFailed = ref(false)
|
const loadFailed = ref(false)
|
||||||
@@ -87,12 +82,9 @@ const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
|||||||
const supportLoading = ref(false)
|
const supportLoading = ref(false)
|
||||||
const showBackTop = ref(false)
|
const showBackTop = ref(false)
|
||||||
const mobilePageSize = 10
|
const mobilePageSize = 10
|
||||||
/** KeepAlive 缓存期间为 false,避免详情页路由变化误触发首页筛选/刷新 */
|
|
||||||
const pageActive = ref(true)
|
|
||||||
let listingRequestSeq = 0
|
let listingRequestSeq = 0
|
||||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let savedScrollY = 0
|
|
||||||
let homeConfigLoaded = false
|
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ key: 'recommended', label: '综合推荐' },
|
{ key: 'recommended', label: '综合推荐' },
|
||||||
@@ -301,77 +293,101 @@ const visibleZoneOptions = computed(() =>
|
|||||||
|
|
||||||
applyRouteQueryToMobileState()
|
applyRouteQueryToMobileState()
|
||||||
|
|
||||||
function isMobileHomeRoute() {
|
function currentListSignature() {
|
||||||
return (
|
return buildMobileHomeListSignature({
|
||||||
route.name === 'mobile-home' ||
|
searchValue: searchValue.value,
|
||||||
route.name === 'mobile-listings' ||
|
activeSort: activeSort.value,
|
||||||
route.path === '/m' ||
|
activeZone: activeZone.value,
|
||||||
route.path === '/m/listings'
|
selectedFilters: selectedFilters.value,
|
||||||
)
|
rangeFilters: rangeFilters.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindScrollListener() {
|
function persistListCache() {
|
||||||
window.addEventListener('scroll', handleWindowScroll, { passive: true })
|
homeCache.saveList({
|
||||||
|
signature: currentListSignature(),
|
||||||
|
listings: listings.value.slice(),
|
||||||
|
totalListings: totalListings.value,
|
||||||
|
zoneCounts: { ...zoneCounts.value },
|
||||||
|
currentPage: currentPage.value,
|
||||||
|
hasMoreListings: hasMoreListings.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function unbindScrollListener() {
|
function persistScrollTop() {
|
||||||
window.removeEventListener('scroll', handleWindowScroll)
|
if (!scrollEl.value) return
|
||||||
|
homeCache.saveScrollTop(scrollEl.value.scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFromCache() {
|
||||||
|
const signature = currentListSignature()
|
||||||
|
if (!homeCache.matchList(signature)) return false
|
||||||
|
// 拷贝快照,避免页面内变更直接写穿 store
|
||||||
|
listings.value = homeCache.listings.slice()
|
||||||
|
totalListings.value = homeCache.totalListings
|
||||||
|
zoneCounts.value = { ...homeCache.zoneCounts }
|
||||||
|
currentPage.value = homeCache.currentPage
|
||||||
|
hasMoreListings.value = homeCache.hasMoreListings
|
||||||
|
loadFailed.value = false
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreHomeConfigFromCache() {
|
||||||
|
if (!homeCache.homeConfigLoaded) return false
|
||||||
|
announcements.value = homeCache.announcements.slice()
|
||||||
|
bannerSlides.value = homeCache.bannerSlides.map(slide => ({ ...slide }))
|
||||||
|
publishOptions.value = homeCache.publishOptions
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreScrollPosition() {
|
function restoreScrollPosition() {
|
||||||
const top = savedScrollY
|
const top = homeCache.scrollTop
|
||||||
const apply = () => window.scrollTo({ top, left: 0, behavior: 'auto' })
|
const apply = () => {
|
||||||
|
if (scrollEl.value) scrollEl.value.scrollTop = top
|
||||||
|
}
|
||||||
apply()
|
apply()
|
||||||
// 布局/图片回流后再校正一次,避免回顶
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
apply()
|
apply()
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
apply()
|
apply()
|
||||||
handleWindowScroll()
|
requestAnimationFrame(() => {
|
||||||
|
apply()
|
||||||
|
handleListScroll()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 首次进入加载;KeepAlive 返回时走 onActivated,不再整页重拉
|
const restoredList = restoreFromCache()
|
||||||
if (!listings.value.length && !loadFailed.value) {
|
if (!restoredList) {
|
||||||
loadListings()
|
loadListings()
|
||||||
}
|
}
|
||||||
if (!homeConfigLoaded) {
|
if (!restoreHomeConfigFromCache()) {
|
||||||
loadHomeConfig()
|
loadHomeConfig()
|
||||||
}
|
}
|
||||||
|
if (restoredList) {
|
||||||
|
restoreScrollPosition()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onBeforeRouteLeave(() => {
|
||||||
pageActive.value = true
|
persistScrollTop()
|
||||||
bindScrollListener()
|
|
||||||
// 缓存实例仍持有列表数据;只把筛选写回 URL,避免整表重拉
|
|
||||||
if (isMobileHomeRoute()) {
|
|
||||||
syncMobileHomeQuery()
|
|
||||||
}
|
|
||||||
restoreScrollPosition()
|
|
||||||
})
|
|
||||||
|
|
||||||
onDeactivated(() => {
|
|
||||||
savedScrollY = window.scrollY || window.pageYOffset || 0
|
|
||||||
pageActive.value = false
|
|
||||||
unbindScrollListener()
|
|
||||||
if (searchDebounceTimer) {
|
|
||||||
clearTimeout(searchDebounceTimer)
|
|
||||||
searchDebounceTimer = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
unbindScrollListener()
|
persistScrollTop()
|
||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => filterQuerySignature(),
|
() => filterQuerySignature(),
|
||||||
() => {
|
() => {
|
||||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
|
||||||
syncMobileHomeQuery()
|
syncMobileHomeQuery()
|
||||||
|
// 筛选变化时清空旧列表缓存,避免返回时命中过期数据
|
||||||
|
homeCache.clearList()
|
||||||
loadListings(true)
|
loadListings(true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -379,17 +395,15 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => route.query,
|
||||||
() => {
|
() => {
|
||||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
|
||||||
applyRouteQueryToMobileState()
|
applyRouteQueryToMobileState()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(searchValue, () => {
|
watch(searchValue, () => {
|
||||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
|
||||||
syncMobileHomeQuery()
|
syncMobileHomeQuery()
|
||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||||
searchDebounceTimer = setTimeout(() => {
|
searchDebounceTimer = setTimeout(() => {
|
||||||
if (!pageActive.value || !isMobileHomeRoute()) return
|
homeCache.clearList()
|
||||||
loadListings(true)
|
loadListings(true)
|
||||||
}, 300)
|
}, 300)
|
||||||
})
|
})
|
||||||
@@ -412,7 +426,12 @@ async function loadListings(reset = true) {
|
|||||||
zoneCounts.value = page.zone_counts
|
zoneCounts.value = page.zone_counts
|
||||||
hasMoreListings.value = listings.value.length < page.total
|
hasMoreListings.value = listings.value.length < page.total
|
||||||
currentPage.value = page.page + 1
|
currentPage.value = page.page + 1
|
||||||
requestAnimationFrame(handleWindowScroll)
|
persistListCache()
|
||||||
|
if (reset) {
|
||||||
|
homeCache.saveScrollTop(0)
|
||||||
|
if (scrollEl.value) scrollEl.value.scrollTop = 0
|
||||||
|
}
|
||||||
|
requestAnimationFrame(handleListScroll)
|
||||||
} catch {
|
} catch {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
listings.value = []
|
listings.value = []
|
||||||
@@ -420,6 +439,7 @@ async function loadListings(reset = true) {
|
|||||||
zoneCounts.value = {}
|
zoneCounts.value = {}
|
||||||
hasMoreListings.value = false
|
hasMoreListings.value = false
|
||||||
loadFailed.value = true
|
loadFailed.value = true
|
||||||
|
homeCache.clearList()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (requestSeq === listingRequestSeq) {
|
if (requestSeq === listingRequestSeq) {
|
||||||
@@ -435,7 +455,11 @@ async function loadHomeConfig() {
|
|||||||
announcements.value = config.announcements
|
announcements.value = config.announcements
|
||||||
bannerSlides.value = config.banners
|
bannerSlides.value = config.banners
|
||||||
publishOptions.value = config.publish_options
|
publishOptions.value = config.publish_options
|
||||||
homeConfigLoaded = true
|
homeCache.saveHomeConfig({
|
||||||
|
announcements: config.announcements,
|
||||||
|
bannerSlides: config.banners,
|
||||||
|
publishOptions: config.publish_options,
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
announcements.value = defaultHomeAnnouncements
|
announcements.value = defaultHomeAnnouncements
|
||||||
bannerSlides.value = defaultHomeBanners
|
bannerSlides.value = defaultHomeBanners
|
||||||
@@ -446,10 +470,16 @@ async function loadHomeConfig() {
|
|||||||
async function onRefresh() {
|
async function onRefresh() {
|
||||||
refreshing.value = true
|
refreshing.value = true
|
||||||
try {
|
try {
|
||||||
|
homeCache.clearList()
|
||||||
const [, nextHomeConfig] = await Promise.all([loadListings(true), fetchMobileHomeConfig()])
|
const [, nextHomeConfig] = await Promise.all([loadListings(true), fetchMobileHomeConfig()])
|
||||||
announcements.value = nextHomeConfig.announcements
|
announcements.value = nextHomeConfig.announcements
|
||||||
bannerSlides.value = nextHomeConfig.banners
|
bannerSlides.value = nextHomeConfig.banners
|
||||||
publishOptions.value = nextHomeConfig.publish_options
|
publishOptions.value = nextHomeConfig.publish_options
|
||||||
|
homeCache.saveHomeConfig({
|
||||||
|
announcements: nextHomeConfig.announcements,
|
||||||
|
bannerSlides: nextHomeConfig.banners,
|
||||||
|
publishOptions: nextHomeConfig.publish_options,
|
||||||
|
})
|
||||||
showToast({ message: '刷新成功', icon: 'passed' })
|
showToast({ message: '刷新成功', icon: 'passed' })
|
||||||
} catch {
|
} catch {
|
||||||
// 静默处理
|
// 静默处理
|
||||||
@@ -578,7 +608,7 @@ function applyRouteQueryToMobileState() {
|
|||||||
decodeHomeQueryJson<Record<string, unknown>>(query, 'ranges', {})
|
decodeHomeQueryJson<Record<string, unknown>>(query, 'ranges', {})
|
||||||
)
|
)
|
||||||
|
|
||||||
// 内容未变时避免赋值,防止 KeepAlive 激活或路由回写时误触发列表刷新
|
// 内容未变时避免赋值,防止 URL 回写时误触发列表刷新
|
||||||
if (
|
if (
|
||||||
searchValue.value === nextSearch &&
|
searchValue.value === nextSearch &&
|
||||||
activeSort.value === nextSort &&
|
activeSort.value === nextSort &&
|
||||||
@@ -637,14 +667,18 @@ function normalizeRangeFilters(value: Record<string, unknown>) {
|
|||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowScroll() {
|
function handleListScroll() {
|
||||||
showBackTop.value = window.scrollY > 520
|
const el = scrollEl.value
|
||||||
if (window.innerHeight + window.scrollY < document.documentElement.scrollHeight - 360) return
|
if (!el) return
|
||||||
|
homeCache.saveScrollTop(el.scrollTop)
|
||||||
|
showBackTop.value = el.scrollTop > 520
|
||||||
|
if (el.scrollTop + el.clientHeight < el.scrollHeight - 360) return
|
||||||
loadListings(false)
|
loadListings(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToTop() {
|
function scrollToTop() {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
scrollEl.value?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
homeCache.saveScrollTop(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSkinGroupKey(key: string) {
|
function isSkinGroupKey(key: string) {
|
||||||
@@ -699,6 +733,7 @@ syncMobileHomeQuery()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="mobile-shell">
|
<main class="mobile-shell">
|
||||||
|
<div ref="scrollEl" class="mobile-scroll" @scroll.passive="handleListScroll">
|
||||||
<!-- ========== Hero 区域:顶部搜索与公告 ========== -->
|
<!-- ========== Hero 区域:顶部搜索与公告 ========== -->
|
||||||
<section class="mobile-hero">
|
<section class="mobile-hero">
|
||||||
<div class="mobile-topbar">
|
<div class="mobile-topbar">
|
||||||
@@ -937,6 +972,18 @@ syncMobileHomeQuery()
|
|||||||
</section>
|
</section>
|
||||||
</van-pull-refresh>
|
</van-pull-refresh>
|
||||||
|
|
||||||
|
<footer class="home-footer">
|
||||||
|
<a
|
||||||
|
href="https://beian.miit.gov.cn/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="home-footer-icp"
|
||||||
|
>湘ICP备2025146668号</a>
|
||||||
|
<span class="home-footer-divider">|</span>
|
||||||
|
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MobileHomeFilterSheet
|
<MobileHomeFilterSheet
|
||||||
v-model:show="filterOpen"
|
v-model:show="filterOpen"
|
||||||
v-model:selected-filters="selectedFilters"
|
v-model:selected-filters="selectedFilters"
|
||||||
@@ -956,17 +1003,6 @@ syncMobileHomeQuery()
|
|||||||
<van-icon name="arrow-up" :size="20" />
|
<van-icon name="arrow-up" :size="20" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<footer class="home-footer">
|
|
||||||
<a
|
|
||||||
href="https://beian.miit.gov.cn/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="home-footer-icp"
|
|
||||||
>湘ICP备2025146668号</a>
|
|
||||||
<span class="home-footer-divider">|</span>
|
|
||||||
<span class="home-footer-copyright">版权所有 ©2026 大锤网络游戏有限公司</span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<MobileBottomNav />
|
<MobileBottomNav />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -33,12 +33,7 @@ import {
|
|||||||
} from '@/features/seller/composables/usePublishDraft'
|
} from '@/features/seller/composables/usePublishDraft'
|
||||||
import type { PublishForm } from '@/shared/types/publish'
|
import type { PublishForm } from '@/shared/types/publish'
|
||||||
import { yuanToCent } from '@/shared/utils/money'
|
import { yuanToCent } from '@/shared/utils/money'
|
||||||
import {
|
import { dailyLossOptions, formatNumber, roundRatio } from '@/shared/utils/pricing'
|
||||||
commonOnlineTimes,
|
|
||||||
dailyLossOptions,
|
|
||||||
formatNumber,
|
|
||||||
roundRatio,
|
|
||||||
} from '@/shared/utils/pricing'
|
|
||||||
|
|
||||||
const draftSaveDelay = 400
|
const draftSaveDelay = 400
|
||||||
const multiScreenshotLimit = 3
|
const multiScreenshotLimit = 3
|
||||||
@@ -104,13 +99,6 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
sellerAgreementChecked.value &&
|
sellerAgreementChecked.value &&
|
||||||
passwordAndDeviceConfirmed.value
|
passwordAndDeviceConfirmed.value
|
||||||
)
|
)
|
||||||
const disabledOnlineStartOptions = computed(() =>
|
|
||||||
commonOnlineTimes.filter(time => !canUseOnlineStart(time))
|
|
||||||
)
|
|
||||||
const disabledOnlineEndOptions = computed(() =>
|
|
||||||
commonOnlineTimes.filter(time => !canUseOnlineEnd(time))
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!isEditMode.value) restoreDraft()
|
if (!isEditMode.value) restoreDraft()
|
||||||
if (options.persistBeforeUnload) window.addEventListener('beforeunload', handleBeforeUnload)
|
if (options.persistBeforeUnload) window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
@@ -360,37 +348,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
return form.online_start === '00:00' && form.online_end === '23:59'
|
return form.online_start === '00:00' && form.online_end === '23:59'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOnlineStartPresetActive(value: string) {
|
|
||||||
return !isAllDayOnline() && form.online_start === value
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOnlineEndPresetActive(value: string) {
|
|
||||||
return !isAllDayOnline() && form.online_end === value
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAllDayOnline() {
|
function selectAllDayOnline() {
|
||||||
form.online_start = '00:00'
|
form.online_start = '00:00'
|
||||||
form.online_end = '23:59'
|
form.online_end = '23:59'
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectOnlineStart(value: string | number) {
|
|
||||||
const time = String(value)
|
|
||||||
if (!canUseOnlineStart(time)) {
|
|
||||||
options.notifyWarning('开始时间必须早于结束时间')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form.online_start = time
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOnlineEnd(value: string | number) {
|
|
||||||
const time = String(value)
|
|
||||||
if (!canUseOnlineEnd(time)) {
|
|
||||||
options.notifyWarning('结束时间必须晚于开始时间')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
form.online_end = time
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOnlineStartChange(value: string | number | null | undefined) {
|
function handleOnlineStartChange(value: string | number | null | undefined) {
|
||||||
const time = normalizeOnlineTimeValue(value)
|
const time = normalizeOnlineTimeValue(value)
|
||||||
form.online_start = time
|
form.online_start = time
|
||||||
@@ -427,22 +389,6 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUseOnlineStart(value: string) {
|
|
||||||
const start = parseOnlineTime(value)
|
|
||||||
const lastTime = parseOnlineTime('23:59')
|
|
||||||
if (start === null || lastTime === null || start >= lastTime) return false
|
|
||||||
const end = parseOnlineTime(form.online_end)
|
|
||||||
return end === null || start < end
|
|
||||||
}
|
|
||||||
|
|
||||||
function canUseOnlineEnd(value: string) {
|
|
||||||
const end = parseOnlineTime(value)
|
|
||||||
const firstTime = parseOnlineTime('00:00')
|
|
||||||
if (end === null || firstTime === null || end <= firstTime) return false
|
|
||||||
const start = parseOnlineTime(form.online_start)
|
|
||||||
return start === null || end > start
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateOnlineTime() {
|
function validateOnlineTime() {
|
||||||
const start = parseOnlineTime(form.online_start)
|
const start = parseOnlineTime(form.online_start)
|
||||||
const end = parseOnlineTime(form.online_end)
|
const end = parseOnlineTime(form.online_end)
|
||||||
@@ -944,7 +890,6 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
return {
|
return {
|
||||||
...pricing,
|
...pricing,
|
||||||
dailyLossOptions,
|
dailyLossOptions,
|
||||||
commonOnlineTimes,
|
|
||||||
formatNumber,
|
formatNumber,
|
||||||
router,
|
router,
|
||||||
loading,
|
loading,
|
||||||
@@ -969,19 +914,13 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
pageTitle,
|
pageTitle,
|
||||||
submitButtonText,
|
submitButtonText,
|
||||||
submitLoadingText,
|
submitLoadingText,
|
||||||
disabledOnlineStartOptions,
|
|
||||||
disabledOnlineEndOptions,
|
|
||||||
loadPublishOptions,
|
loadPublishOptions,
|
||||||
saveDraft,
|
saveDraft,
|
||||||
handleSaveDraft,
|
handleSaveDraft,
|
||||||
resetDraftState,
|
resetDraftState,
|
||||||
handleResetDraft,
|
handleResetDraft,
|
||||||
isAllDayOnline,
|
isAllDayOnline,
|
||||||
isOnlineStartPresetActive,
|
|
||||||
isOnlineEndPresetActive,
|
|
||||||
selectAllDayOnline,
|
selectAllDayOnline,
|
||||||
selectOnlineStart,
|
|
||||||
selectOnlineEnd,
|
|
||||||
handleOnlineStartChange,
|
handleOnlineStartChange,
|
||||||
handleOnlineEndChange,
|
handleOnlineEndChange,
|
||||||
toggleSkin,
|
toggleSkin,
|
||||||
@@ -1025,5 +964,9 @@ function parseOnlineTime(value: string) {
|
|||||||
|
|
||||||
function normalizeOnlineTimeValue(value: string | number | null | undefined) {
|
function normalizeOnlineTimeValue(value: string | number | null | undefined) {
|
||||||
if (value === null || value === undefined) return ''
|
if (value === null || value === undefined) return ''
|
||||||
return String(value)
|
const raw = String(value).trim()
|
||||||
|
// 兼容原生 time 可能带秒:08:00:00 -> 08:00
|
||||||
|
const match = /^(\d{1,2}):(\d{2})(?::\d{2})?/.exec(raw)
|
||||||
|
if (!match?.[1] || !match[2]) return raw
|
||||||
|
return `${match[1].padStart(2, '0')}:${match[2]}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,24 +200,58 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-row,
|
|
||||||
.result-grid {
|
.result-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-field-group {
|
.online-time-field :deep(.van-field__control) {
|
||||||
display: grid;
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-time-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-time-range {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-field-group .publish-field {
|
.online-time-input {
|
||||||
margin-bottom: 0;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-preset-chips {
|
.online-time-input:focus {
|
||||||
padding: 0 2px 4px;
|
outline: none;
|
||||||
|
border-color: #ffb074;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-time-sep {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-panel {
|
.level-panel {
|
||||||
@@ -832,11 +866,7 @@
|
|||||||
background: #ff6a00 !important;
|
background: #ff6a00 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 430px) {
|
|
||||||
.time-row {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 370px) {
|
@media (max-width: 370px) {
|
||||||
.level-options,
|
.level-options,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import GuideImage from '@/components/GuideImage.vue'
|
|||||||
import tencentSecurityGuide from '@/assets/tencent-security-guide.webp'
|
import tencentSecurityGuide from '@/assets/tencent-security-guide.webp'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commonOnlineTimes,
|
|
||||||
dailyLossOptions,
|
dailyLossOptions,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
router,
|
router,
|
||||||
@@ -32,8 +31,6 @@ const {
|
|||||||
quantityValues,
|
quantityValues,
|
||||||
quantityModes,
|
quantityModes,
|
||||||
selectedSkins,
|
selectedSkins,
|
||||||
disabledOnlineStartOptions,
|
|
||||||
disabledOnlineEndOptions,
|
|
||||||
serverOptions,
|
serverOptions,
|
||||||
faceOptions,
|
faceOptions,
|
||||||
rankOptions,
|
rankOptions,
|
||||||
@@ -73,11 +70,7 @@ const {
|
|||||||
canAddScreenshot,
|
canAddScreenshot,
|
||||||
getScreenshotLimitHint,
|
getScreenshotLimitHint,
|
||||||
isAllDayOnline,
|
isAllDayOnline,
|
||||||
isOnlineStartPresetActive,
|
|
||||||
isOnlineEndPresetActive,
|
|
||||||
selectAllDayOnline,
|
selectAllDayOnline,
|
||||||
selectOnlineStart,
|
|
||||||
selectOnlineEnd,
|
|
||||||
handleOnlineStartChange,
|
handleOnlineStartChange,
|
||||||
handleOnlineEndChange,
|
handleOnlineEndChange,
|
||||||
handleFireLevelInput,
|
handleFireLevelInput,
|
||||||
@@ -386,17 +379,9 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
|
|
||||||
<div class="time-row">
|
<van-field label="在线时间" required class="publish-field online-time-field">
|
||||||
<div class="time-field-group">
|
<template #input>
|
||||||
<van-field
|
<div class="online-time-controls">
|
||||||
v-model="form.online_start"
|
|
||||||
label="在线开始"
|
|
||||||
required
|
|
||||||
type="time"
|
|
||||||
class="publish-field time-field"
|
|
||||||
@update:model-value="handleOnlineStartChange"
|
|
||||||
/>
|
|
||||||
<div class="radio-group small time-preset-chips">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="radio-btn"
|
class="radio-btn"
|
||||||
@@ -405,57 +390,26 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
>
|
>
|
||||||
全天
|
全天
|
||||||
</button>
|
</button>
|
||||||
<button
|
<div class="online-time-range">
|
||||||
v-for="opt in commonOnlineTimes"
|
<input
|
||||||
:key="`start-${opt}`"
|
v-model="form.online_start"
|
||||||
type="button"
|
type="time"
|
||||||
class="radio-btn"
|
class="online-time-input"
|
||||||
:class="{
|
aria-label="在线开始时间"
|
||||||
active: isOnlineStartPresetActive(opt),
|
@change="handleOnlineStartChange(form.online_start)"
|
||||||
disabled: disabledOnlineStartOptions.includes(opt),
|
/>
|
||||||
}"
|
<span class="online-time-sep">至</span>
|
||||||
:disabled="disabledOnlineStartOptions.includes(opt)"
|
<input
|
||||||
@click="selectOnlineStart(opt)"
|
v-model="form.online_end"
|
||||||
>
|
type="time"
|
||||||
{{ opt }}
|
class="online-time-input"
|
||||||
</button>
|
aria-label="在线结束时间"
|
||||||
|
@change="handleOnlineEndChange(form.online_end)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="time-field-group">
|
</van-field>
|
||||||
<van-field
|
|
||||||
v-model="form.online_end"
|
|
||||||
label="在线结束"
|
|
||||||
required
|
|
||||||
type="time"
|
|
||||||
class="publish-field time-field"
|
|
||||||
@update:model-value="handleOnlineEndChange"
|
|
||||||
/>
|
|
||||||
<div class="radio-group small time-preset-chips">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="radio-btn"
|
|
||||||
:class="{ active: isAllDayOnline() }"
|
|
||||||
@click="selectAllDayOnline"
|
|
||||||
>
|
|
||||||
全天
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="opt in commonOnlineTimes"
|
|
||||||
:key="`end-${opt}`"
|
|
||||||
type="button"
|
|
||||||
class="radio-btn"
|
|
||||||
:class="{
|
|
||||||
active: isOnlineEndPresetActive(opt),
|
|
||||||
disabled: disabledOnlineEndOptions.includes(opt),
|
|
||||||
}"
|
|
||||||
:disabled="disabledOnlineEndOptions.includes(opt)"
|
|
||||||
@click="selectOnlineEnd(opt)"
|
|
||||||
>
|
|
||||||
{{ opt }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="field-hint">
|
<p class="field-hint">
|
||||||
此在线时间指的是百分百能够联系上您的时间,若是在此期间联系不上您导致无法上号会扣除您的部分订单金额或上架押金,在线时长太短可能无法上架,请预留充足时间用于扫码以及冻结人脸,请谨慎填写
|
此在线时间指的是百分百能够联系上您的时间,若是在此期间联系不上您导致无法上号会扣除您的部分订单金额或上架押金,在线时长太短可能无法上架,请预留充足时间用于扫码以及冻结人脸,请谨慎填写
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -72,7 +72,6 @@
|
|||||||
.skin-group,
|
.skin-group,
|
||||||
.region-panel,
|
.region-panel,
|
||||||
.ratio-panel,
|
.ratio-panel,
|
||||||
.time-preset-row,
|
|
||||||
.price-cell {
|
.price-cell {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--radius-8);
|
border-radius: var(--radius-8);
|
||||||
@@ -98,7 +97,6 @@
|
|||||||
|
|
||||||
.field-row label span,
|
.field-row label span,
|
||||||
.input-block b,
|
.input-block b,
|
||||||
.time-preset-row strong span,
|
|
||||||
.quantity-meta span,
|
.quantity-meta span,
|
||||||
.upload-copy span {
|
.upload-copy span {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
@@ -178,7 +176,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.level-panel,
|
.level-panel,
|
||||||
.time-preset-panel,
|
|
||||||
.skin-groups {
|
.skin-groups {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -371,41 +368,41 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-preset-row {
|
.online-time-row {
|
||||||
display: grid;
|
align-items: center;
|
||||||
grid-template-columns: 88px minmax(0, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-preset-row strong {
|
.online-time-controls {
|
||||||
color: var(--color-text-main);
|
display: flex;
|
||||||
font-size: 13px;
|
flex-wrap: wrap;
|
||||||
font-weight: 800;
|
align-items: center;
|
||||||
line-height: 32px;
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-preset-content {
|
.online-time-range {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-preset-actions {
|
.online-time-sep {
|
||||||
display: flex;
|
flex: 0 0 auto;
|
||||||
flex-wrap: wrap;
|
color: #94a3b8;
|
||||||
gap: 7px;
|
font-size: 13px;
|
||||||
min-width: 0;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-chip {
|
.time-chip {
|
||||||
min-height: 30px;
|
flex: 0 0 auto;
|
||||||
padding: 0 12px;
|
min-height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
border: 1px solid #d6dce5;
|
border: 1px solid #d6dce5;
|
||||||
border-radius: 15px;
|
border-radius: 999px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #596474;
|
color: #596474;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -414,14 +411,23 @@
|
|||||||
color 0.16s ease;
|
color 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-chip:hover {
|
||||||
|
border-color: #c4ccd8;
|
||||||
|
}
|
||||||
|
|
||||||
.time-chip.active {
|
.time-chip.active {
|
||||||
border-color: var(--color-orange-primary);
|
border-color: var(--color-orange-primary);
|
||||||
background: var(--color-orange-primary);
|
background: var(--color-orange-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 10px rgba(255, 106, 0, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input {
|
.time-input {
|
||||||
width: 118px;
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-time-range :deep(.el-input__wrapper) {
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-region-panel {
|
.login-region-panel {
|
||||||
@@ -927,12 +933,21 @@
|
|||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.field-row,
|
.field-row,
|
||||||
.time-preset-row,
|
|
||||||
.quantity-header,
|
.quantity-header,
|
||||||
.quantity-item {
|
.quantity-item {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.online-time-range {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input {
|
||||||
|
flex: 1;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.field-hint {
|
.field-hint {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import GuideImage from '@/components/GuideImage.vue'
|
|||||||
import tencentSecurityGuide from '@/assets/tencent-security-guide.webp'
|
import tencentSecurityGuide from '@/assets/tencent-security-guide.webp'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commonOnlineTimes,
|
|
||||||
dailyLossOptions,
|
dailyLossOptions,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
loading,
|
loading,
|
||||||
@@ -39,8 +38,6 @@ const {
|
|||||||
requiredScreenshotCount,
|
requiredScreenshotCount,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
submitButtonText,
|
submitButtonText,
|
||||||
disabledOnlineStartOptions,
|
|
||||||
disabledOnlineEndOptions,
|
|
||||||
serverOptions,
|
serverOptions,
|
||||||
faceOptions,
|
faceOptions,
|
||||||
rankOptions,
|
rankOptions,
|
||||||
@@ -85,8 +82,6 @@ const {
|
|||||||
getScreenshotLimitHint,
|
getScreenshotLimitHint,
|
||||||
isAllDayOnline,
|
isAllDayOnline,
|
||||||
selectAllDayOnline,
|
selectAllDayOnline,
|
||||||
selectOnlineStart,
|
|
||||||
selectOnlineEnd,
|
|
||||||
handleOnlineStartChange,
|
handleOnlineStartChange,
|
||||||
handleOnlineEndChange,
|
handleOnlineEndChange,
|
||||||
handleFireLevelInput,
|
handleFireLevelInput,
|
||||||
@@ -344,81 +339,51 @@ function selectDailyLoss(value: string | number) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="time-preset-panel">
|
<div class="field-row panel-field-row online-time-row">
|
||||||
<div class="time-preset-row">
|
<label>在线时间<span>*</span></label>
|
||||||
<strong>在线开始<span>*</span></strong>
|
<div class="online-time-controls">
|
||||||
<div class="time-preset-content">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="time-chip"
|
||||||
|
:class="{ active: isAllDayOnline() }"
|
||||||
|
@click="selectAllDayOnline"
|
||||||
|
>
|
||||||
|
全天
|
||||||
|
</button>
|
||||||
|
<div class="online-time-range">
|
||||||
<el-time-picker
|
<el-time-picker
|
||||||
v-model="form.online_start"
|
v-model="form.online_start"
|
||||||
value-format="HH:mm"
|
value-format="HH:mm"
|
||||||
format="HH:mm"
|
format="HH:mm"
|
||||||
placeholder="开始时间"
|
placeholder="开始"
|
||||||
class="time-input"
|
class="time-input"
|
||||||
@change="handleOnlineStartChange"
|
@change="handleOnlineStartChange"
|
||||||
/>
|
/>
|
||||||
<div class="time-preset-actions">
|
<span class="online-time-sep">至</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="time-chip"
|
|
||||||
:class="{ active: isAllDayOnline() }"
|
|
||||||
@click="selectAllDayOnline"
|
|
||||||
>
|
|
||||||
全天
|
|
||||||
</button>
|
|
||||||
<OptionChips
|
|
||||||
:options="commonOnlineTimes"
|
|
||||||
:model-value="isAllDayOnline() ? '' : form.online_start"
|
|
||||||
:disabled-values="disabledOnlineStartOptions"
|
|
||||||
key-prefix="start-"
|
|
||||||
@select="selectOnlineStart"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="time-preset-row">
|
|
||||||
<strong>在线结束<span>*</span></strong>
|
|
||||||
<div class="time-preset-content">
|
|
||||||
<el-time-picker
|
<el-time-picker
|
||||||
v-model="form.online_end"
|
v-model="form.online_end"
|
||||||
value-format="HH:mm"
|
value-format="HH:mm"
|
||||||
format="HH:mm"
|
format="HH:mm"
|
||||||
placeholder="结束时间"
|
placeholder="结束"
|
||||||
class="time-input"
|
class="time-input"
|
||||||
@change="handleOnlineEndChange"
|
@change="handleOnlineEndChange"
|
||||||
/>
|
/>
|
||||||
<div class="time-preset-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="time-chip"
|
|
||||||
:class="{ active: isAllDayOnline() }"
|
|
||||||
@click="selectAllDayOnline"
|
|
||||||
>
|
|
||||||
全天
|
|
||||||
</button>
|
|
||||||
<OptionChips
|
|
||||||
:options="commonOnlineTimes"
|
|
||||||
:model-value="isAllDayOnline() ? '' : form.online_end"
|
|
||||||
:disabled-values="disabledOnlineEndOptions"
|
|
||||||
key-prefix="end-"
|
|
||||||
@select="selectOnlineEnd"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="field-hint">请填写能稳定联系上您的时间,便于扫码、冻结人脸和订单交接。</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="field-hint">请填写能稳定联系上您的时间,便于扫码、冻结人脸和订单交接。</p>
|
|
||||||
|
|
||||||
<div v-if="banRecordOptions.length" class="field-row">
|
<div v-if="banRecordOptions.length" class="field-row panel-field-row">
|
||||||
<label>封禁记录<span>*</span></label>
|
<label>封禁记录<span>*</span></label>
|
||||||
<OptionChips
|
<OptionChips
|
||||||
:options="banRecordOptions"
|
:options="banRecordOptions"
|
||||||
:model-value="form.ban_record"
|
:model-value="form.ban_record"
|
||||||
@select="form.ban_record = String($event)"
|
@select="form.ban_record = String($event)"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="showBanRecordRiskHint" class="field-hint">
|
||||||
|
刚出租结算完的账号需冷号七天才可再次出租,IP频繁变动会导致封号。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="showBanRecordRiskHint && banRecordOptions.length" class="field-hint">
|
|
||||||
刚出租结算完的账号需冷号七天才可再次出租,IP频繁变动会导致封号。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-if="regionOptions.length" class="region-panel login-region-panel">
|
<div v-if="regionOptions.length" class="region-panel login-region-panel">
|
||||||
<div class="region-title">
|
<div class="region-title">
|
||||||
|
|||||||
@@ -14,13 +14,9 @@ import { sellerRoutes } from './sellerRoutes'
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [...adminRoutes, ...mobileRoutes, ...publicRoutes, ...accountRoutes, ...sellerRoutes],
|
routes: [...adminRoutes, ...mobileRoutes, ...publicRoutes, ...accountRoutes, ...sellerRoutes],
|
||||||
scrollBehavior(to, _from, savedPosition) {
|
scrollBehavior(_to, _from, savedPosition) {
|
||||||
// 浏览器前进/后退优先用历史滚动位置
|
// 浏览器前进/后退用历史滚动;移动端首页列表在内部容器滚动,不依赖 window
|
||||||
if (savedPosition) return savedPosition
|
if (savedPosition) return savedPosition
|
||||||
// 移动端首页由 KeepAlive + 组件内自行恢复滚动,避免被强制拉回顶部
|
|
||||||
if (to.name === 'mobile-home' || to.name === 'mobile-listings') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return { left: 0, top: 0 }
|
return { left: 0, top: 0 }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,17 +14,6 @@ import type {
|
|||||||
} from '@/shared/types/publish'
|
} from '@/shared/types/publish'
|
||||||
|
|
||||||
export const dailyLossOptions = [10, 20, 30, 40, 50]
|
export const dailyLossOptions = [10, 20, 30, 40, 50]
|
||||||
export const commonOnlineTimes = [
|
|
||||||
'00:00',
|
|
||||||
'08:00',
|
|
||||||
'10:00',
|
|
||||||
'12:00',
|
|
||||||
'14:00',
|
|
||||||
'18:00',
|
|
||||||
'20:00',
|
|
||||||
'22:00',
|
|
||||||
'23:59',
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将金额四舍五入到角精度(0.1元)
|
* 将金额四舍五入到角精度(0.1元)
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultHomeAnnouncements,
|
||||||
|
defaultHomeBanners,
|
||||||
|
type HomeBannerSlide,
|
||||||
|
} from '@/features/listings/api/homeConfig'
|
||||||
|
import {
|
||||||
|
emptyListingPublishOptions,
|
||||||
|
type ListingPublishOptions,
|
||||||
|
} from '@/features/listings/api/listingOptions'
|
||||||
|
import type { Listing } from '@/features/listings/api/listings'
|
||||||
|
|
||||||
|
export type MobileHomeRangeFilters = Record<string, { min: string; max: string }>
|
||||||
|
|
||||||
|
export function buildMobileHomeListSignature(input: {
|
||||||
|
searchValue: string
|
||||||
|
activeSort: string
|
||||||
|
activeZone: string
|
||||||
|
selectedFilters: Record<string, string[]>
|
||||||
|
rangeFilters: MobileHomeRangeFilters
|
||||||
|
}) {
|
||||||
|
return JSON.stringify({
|
||||||
|
keyword: input.searchValue.trim(),
|
||||||
|
sort: input.activeSort,
|
||||||
|
zone: input.activeZone,
|
||||||
|
selected: input.selectedFilters,
|
||||||
|
ranges: input.rangeFilters,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移动端首页列表/配置/滚动缓存:与页面实例解耦,返回详情后可直接恢复 */
|
||||||
|
export const useMobileHomeCacheStore = defineStore('mobileHomeCache', {
|
||||||
|
state: () => ({
|
||||||
|
/** 与当前 listings 数据对应的筛选签名;空字符串表示尚无可用快照 */
|
||||||
|
listSignature: '',
|
||||||
|
listings: [] as Listing[],
|
||||||
|
totalListings: 0,
|
||||||
|
zoneCounts: {} as Record<string, number>,
|
||||||
|
/** 下一页页码(与 MobileHomeView.currentPage 语义一致) */
|
||||||
|
currentPage: 1,
|
||||||
|
hasMoreListings: true,
|
||||||
|
/** 列表滚动容器 scrollTop */
|
||||||
|
scrollTop: 0,
|
||||||
|
announcements: [...defaultHomeAnnouncements] as string[],
|
||||||
|
bannerSlides: defaultHomeBanners.map(slide => ({ ...slide })) as HomeBannerSlide[],
|
||||||
|
publishOptions: { ...emptyListingPublishOptions } as ListingPublishOptions,
|
||||||
|
homeConfigLoaded: false,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
hasListSnapshot(state): boolean {
|
||||||
|
return state.listSignature !== ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
matchList(signature: string) {
|
||||||
|
return this.listSignature !== '' && this.listSignature === signature
|
||||||
|
},
|
||||||
|
saveList(payload: {
|
||||||
|
signature: string
|
||||||
|
listings: Listing[]
|
||||||
|
totalListings: number
|
||||||
|
zoneCounts: Record<string, number>
|
||||||
|
currentPage: number
|
||||||
|
hasMoreListings: boolean
|
||||||
|
}) {
|
||||||
|
this.listSignature = payload.signature
|
||||||
|
this.listings = payload.listings
|
||||||
|
this.totalListings = payload.totalListings
|
||||||
|
this.zoneCounts = payload.zoneCounts
|
||||||
|
this.currentPage = payload.currentPage
|
||||||
|
this.hasMoreListings = payload.hasMoreListings
|
||||||
|
},
|
||||||
|
saveScrollTop(top: number) {
|
||||||
|
this.scrollTop = Math.max(0, top)
|
||||||
|
},
|
||||||
|
saveHomeConfig(payload: {
|
||||||
|
announcements: string[]
|
||||||
|
bannerSlides: HomeBannerSlide[]
|
||||||
|
publishOptions: ListingPublishOptions
|
||||||
|
}) {
|
||||||
|
this.announcements = payload.announcements
|
||||||
|
this.bannerSlides = payload.bannerSlides
|
||||||
|
this.publishOptions = payload.publishOptions
|
||||||
|
this.homeConfigLoaded = true
|
||||||
|
},
|
||||||
|
clearList() {
|
||||||
|
this.listSignature = ''
|
||||||
|
this.listings = []
|
||||||
|
this.totalListings = 0
|
||||||
|
this.zoneCounts = {}
|
||||||
|
this.currentPage = 1
|
||||||
|
this.hasMoreListings = true
|
||||||
|
this.scrollTop = 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user