fix(systemconfig): 首页入口展示开关与发布选项编码修复

This commit is contained in:
yml2213
2026-10-22 20:25:57 +08:00
parent 51855a0e17
commit 2ce3320d5f
16 changed files with 591 additions and 54 deletions
@@ -38,6 +38,33 @@
gap: 10px;
}
.home-entry-switch-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border: 1px solid #e5edf8;
border-radius: 8px;
background: #f8fbff;
}
.home-entry-switch-row > div {
display: grid;
gap: 4px;
}
.home-entry-switch-row strong {
color: #111827;
font-size: 14px;
}
.home-entry-switch-row span {
color: #6b7280;
font-size: 12px;
line-height: 1.5;
}
.welcome-config-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -17,15 +17,21 @@ import {
type HomeBannerSlide,
} from '@/features/listings/api/homeConfig'
import type { OrderAgreements } from '@/features/orders/api/orders'
import { fetchSystemConfigs, type SystemConfig } from '@/features/admin/api/systemConfigs'
import {
fetchSystemConfigs,
updateSystemConfig,
type SystemConfig,
} from '@/features/admin/api/systemConfigs'
import { parseRenterGrowthConfig } from '@/features/admin/api/renterGrowth'
import {
parseSmsBusinessTemplatesConfig,
smsNodeMetas,
} from '@/features/admin/api/smsNodeTemplates'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
import { safeParseJSON } from '@/shared/utils/json'
import { formatSystemConfigSelectValue } from '@/shared/utils/systemConfigOptions'
import { ElMessage } from 'element-plus'
// 导入重构后的 Dialog 子组件
import PublishOptionsDialog from '../components/PublishOptionsDialog.vue'
@@ -120,6 +126,14 @@ const awRecycleConfig = computed(
const cooperationFeedbackConfig = computed(
() => configs.value.find(item => item.key === 'mobile.cooperation_feedback') || null
)
const homeEntriesEnabledConfig = computed(
() => configs.value.find(item => item.key === 'mobile.home_entries_enabled') || null
)
const homeEntriesDisplayEnabled = computed(() => {
const raw = homeEntriesEnabledConfig.value?.value.trim().toLowerCase() || ''
return raw === 'true' || raw === '1'
})
const homeEntriesSwitchSaving = ref(false)
const listingPublishAgreementsConfig = computed(
() => configs.value.find(item => item.key === 'listing.publish_agreements') || null
)
@@ -167,6 +181,7 @@ const regularConfigs = computed(() =>
item.key !== 'mobile.home_banners' &&
item.key !== 'mobile.aw_recycle' &&
item.key !== 'mobile.cooperation_feedback' &&
item.key !== 'mobile.home_entries_enabled' &&
item.key !== 'listing.publish_agreements' &&
item.key !== 'order.agreements' &&
item.key !== 'profile.post_rental_notice' &&
@@ -216,6 +231,7 @@ const homeStats = computed(() => {
const awRecycleStats = computed(() => {
const item = parseAWRecycle(awRecycleConfig.value?.value || '')
if (!homeEntriesDisplayEnabled.value) return { enabledLabel: '已隐藏' }
return {
enabledLabel: item.enabled ? (item.image_url ? '已配置图' : '已开启') : '已隐藏',
}
@@ -223,6 +239,7 @@ const awRecycleStats = computed(() => {
const cooperationFeedbackStats = computed(() => {
const item = parseCooperationFeedback(cooperationFeedbackConfig.value?.value || '')
if (!homeEntriesDisplayEnabled.value) return { enabledLabel: '已隐藏' }
return {
enabledLabel: item.enabled ? (item.image_url ? '已配置图' : '已开启') : '已隐藏',
}
@@ -288,6 +305,25 @@ async function loadConfigs() {
}
}
async function handleHomeEntriesDisplayChange(value: string | number | boolean) {
const row = homeEntriesEnabledConfig.value
if (!row || homeEntriesSwitchSaving.value) return
const next = value === true || value === 1 || value === 'true'
homeEntriesSwitchSaving.value = true
try {
await updateSystemConfig(row.key, {
value: next ? 'true' : 'false',
description: row.description,
})
await loadConfigs()
ElMessage.success(next ? '首页业务入口已恢复展示' : '首页业务入口已隐藏')
} catch (error) {
ElMessage.error(readError(error, '保存失败'))
} finally {
homeEntriesSwitchSaving.value = false
}
}
function systemConfigByKey(key: string, fallbackValue: string, description: string): SystemConfig {
const existing = configs.value.find(item => item.key === key)
if (existing) return existing
@@ -654,7 +690,11 @@ function formatConfigValue(row: SystemConfig) {
<section
v-if="
homeAnnouncementsConfig || homeBannersConfig || awRecycleConfig || cooperationFeedbackConfig
homeAnnouncementsConfig ||
homeBannersConfig ||
awRecycleConfig ||
cooperationFeedbackConfig ||
homeEntriesEnabledConfig
"
class="publish-config-panel"
>
@@ -685,6 +725,23 @@ function formatConfigValue(row: SystemConfig) {
>
</div>
</div>
<div class="home-entry-switch-row">
<div>
<strong>首页业务入口展示</strong>
<span
>总开关关闭后,PC
与移动端同时隐藏「AW炫彩回收」「合作与反馈」;开启后按各入口自己的启用状态展示。</span
>
</div>
<el-switch
:model-value="homeEntriesDisplayEnabled"
:loading="homeEntriesSwitchSaving"
:disabled="!homeEntriesEnabledConfig || homeEntriesSwitchSaving"
active-text="展示"
inactive-text="隐藏"
@change="handleHomeEntriesDisplayChange"
/>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ homeStats.announcementCount }}</strong>
@@ -129,6 +129,12 @@ describe('mergeHomeConfig', () => {
expect(config.banners.length).toBeGreaterThan(0)
expect(config.banners[0]?.pc.image_url).toBe('')
})
it('首页业务入口总开关缺省关闭,显式开启时透传', () => {
expect(mergeHomeConfig({}).home_entries_enabled).toBe(false)
expect(mergeHomeConfig({ home_entries_enabled: true }).home_entries_enabled).toBe(true)
expect(mergeHomeConfig({ home_entries_enabled: false }).home_entries_enabled).toBe(false)
})
})
describe('createBannerHotspot', () => {
@@ -209,8 +209,13 @@ export interface MobileHomeConfig {
publish_options: ListingPublishOptions
aw_recycle: AWRecycleConfig
cooperation_feedback: CooperationFeedbackConfig
/** 首页业务入口总开关:关闭后 PC/移动端同时隐藏回收与合作入口。 */
home_entries_enabled: boolean
}
/** 总开关默认关闭,避免新环境误展示尚未准备好的业务入口。 */
export const defaultHomeEntriesEnabled = false
export const defaultAWRecycleConfig: AWRecycleConfig = {
title: 'AW炫彩回收',
subtitle: '点击查看回收说明',
@@ -320,6 +325,10 @@ export function mergeHomeConfig(config?: Partial<MobileHomeConfig>): MobileHomeC
publish_options: mergeListingPublishOptions(config?.publish_options),
aw_recycle: awRecycle,
cooperation_feedback: cooperationFeedback,
home_entries_enabled:
typeof config?.home_entries_enabled === 'boolean'
? config.home_entries_enabled
: defaultHomeEntriesEnabled,
}
}
@@ -7,6 +7,7 @@ import {
defaultCooperationFeedbackConfig,
defaultHomeAnnouncements,
defaultHomeBanners,
defaultHomeEntriesEnabled,
fetchMobileHomeConfig,
normalizeHomeBanner,
type AWRecycleConfig,
@@ -71,6 +72,20 @@ const awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
const awRecycleDialogVisible = ref(false)
const cooperationFeedback = ref<CooperationFeedbackConfig>({ ...defaultCooperationFeedbackConfig })
const cooperationFeedbackDialogVisible = ref(false)
const homeEntriesEnabled = ref(defaultHomeEntriesEnabled)
const showAWRecycle = computed(() => homeEntriesEnabled.value && awRecycle.value.enabled)
const showCooperationFeedback = computed(
() => homeEntriesEnabled.value && cooperationFeedback.value.enabled
)
const showBizEntryRail = computed(
() => !siteStore.isDistribution || showAWRecycle.value || showCooperationFeedback.value
)
const bizEntryCount = computed(
() =>
(siteStore.isDistribution ? 0 : 1) +
(showAWRecycle.value ? 1 : 0) +
(showCooperationFeedback.value ? 1 : 0)
)
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
interface SortButtonItem {
key: string
@@ -251,12 +266,14 @@ async function loadHome() {
banners.value = config.banners
awRecycle.value = config.aw_recycle
cooperationFeedback.value = config.cooperation_feedback
homeEntriesEnabled.value = config.home_entries_enabled
publishOptions.value = config.publish_options
} catch {
announcements.value = defaultHomeAnnouncements
banners.value = defaultHomeBanners
awRecycle.value = { ...defaultAWRecycleConfig }
cooperationFeedback.value = { ...defaultCooperationFeedbackConfig }
homeEntriesEnabled.value = defaultHomeEntriesEnabled
publishOptions.value = emptyListingPublishOptions
} finally {
loading.value = false
@@ -404,6 +421,7 @@ function restoreFromCache(): boolean {
banners.value = pcHomeCache.banners.map(slide => normalizeHomeBanner(slide))
awRecycle.value = { ...pcHomeCache.awRecycle }
cooperationFeedback.value = { ...pcHomeCache.cooperationFeedback }
homeEntriesEnabled.value = pcHomeCache.homeEntriesEnabled
publishOptions.value = pcHomeCache.publishOptions
}
return true
@@ -425,6 +443,7 @@ function saveToCache() {
banners: banners.value,
awRecycle: awRecycle.value,
cooperationFeedback: cooperationFeedback.value,
homeEntriesEnabled: homeEntriesEnabled.value,
publishOptions: publishOptions.value,
})
pcHomeCache.saveScrollTop(window.scrollY)
@@ -539,11 +558,16 @@ if (!restored) {
</div>
</div>
<div class="hero-section">
<div class="hero-section" :class="{ 'without-entry-rail': !showBizEntryRail }">
<HomeBanner :banners="displayBanners" :stats="statCards" />
<!-- 业务入口:贴在轮播右侧竖排;原「租号大厅」只是跳回首页,已移除 -->
<div class="biz-entry-rail" aria-label="业务入口">
<div
v-if="showBizEntryRail"
class="biz-entry-rail"
:class="{ 'single-entry': bizEntryCount === 1 }"
aria-label="业务入口"
>
<RouterLink v-if="!siteStore.isDistribution" class="biz-entry crash" to="/crash">
<div class="biz-title-row">
<strong>撞车 <em>NEW</em></strong>
@@ -552,7 +576,7 @@ if (!restored) {
<van-icon name="arrow" :size="14" class="biz-arrow" />
</RouterLink>
<button
v-if="awRecycle.enabled"
v-if="showAWRecycle"
type="button"
class="biz-entry aw-recycle"
@click="openAWRecycle"
@@ -564,7 +588,7 @@ if (!restored) {
<van-icon name="arrow" :size="14" class="biz-arrow" />
</button>
<button
v-if="cooperationFeedback.enabled"
v-if="showCooperationFeedback"
type="button"
class="biz-entry cooperation-feedback"
@click="openCooperationFeedback"
@@ -699,6 +723,10 @@ if (!restored) {
min-width: 0;
}
.hero-section.without-entry-rail {
grid-template-columns: minmax(0, 1fr);
}
.biz-entry-rail {
display: grid;
grid-auto-rows: minmax(0, 1fr);
@@ -706,6 +734,11 @@ if (!restored) {
min-width: 0;
}
.biz-entry-rail.single-entry {
grid-auto-rows: auto;
align-content: start;
}
.biz-entry {
position: relative;
display: flex;
@@ -843,6 +876,14 @@ if (!restored) {
gap: 10px;
}
.biz-entry-rail.single-entry {
grid-template-columns: minmax(0, 1fr);
}
.biz-entry-rail.two-entries {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.biz-entry {
min-height: 76px;
padding: 14px 40px 14px 16px;
@@ -493,6 +493,14 @@
margin: 0 0 12px;
}
.biz-entry-grid.single-entry {
grid-template-columns: minmax(0, 1fr);
}
.biz-entry-grid.two-entries {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.biz-entry {
position: relative;
display: flex;
@@ -18,6 +18,7 @@ import {
defaultCooperationFeedbackConfig,
defaultHomeAnnouncements,
defaultHomeBanners,
defaultHomeEntriesEnabled,
fetchMobileHomeConfig,
hasBannerImage,
normalizeHomeBanner,
@@ -126,6 +127,21 @@ const awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
const awRecyclePreviewVisible = ref(false)
const cooperationFeedback = ref<CooperationFeedbackConfig>({ ...defaultCooperationFeedbackConfig })
const cooperationFeedbackPreviewVisible = ref(false)
const homeEntriesEnabled = ref(defaultHomeEntriesEnabled)
const showAWRecycle = computed(() => homeEntriesEnabled.value && awRecycle.value.enabled)
const showCooperationFeedback = computed(
() => homeEntriesEnabled.value && cooperationFeedback.value.enabled
)
const showCrashEntry = computed(() => !siteStore.isDistribution)
const showBizEntryGrid = computed(
() => showCrashEntry.value || showAWRecycle.value || showCooperationFeedback.value
)
const bizEntryCount = computed(
() =>
(showCrashEntry.value ? 1 : 0) +
(showAWRecycle.value ? 1 : 0) +
(showCooperationFeedback.value ? 1 : 0)
)
const supportLoading = ref(false)
const showBackTop = ref(false)
const mobilePageSize = 10
@@ -361,6 +377,9 @@ function restoreHomeConfigFromCache() {
if (!homeCache.homeConfigLoaded) return false
announcements.value = homeCache.announcements.slice()
bannerSlides.value = homeCache.bannerSlides.map(slide => normalizeHomeBanner(slide))
awRecycle.value = { ...homeCache.awRecycle }
cooperationFeedback.value = { ...homeCache.cooperationFeedback }
homeEntriesEnabled.value = homeCache.homeEntriesEnabled
publishOptions.value = homeCache.publishOptions
return true
}
@@ -479,10 +498,14 @@ async function loadHomeConfig() {
bannerSlides.value = config.banners
awRecycle.value = config.aw_recycle
cooperationFeedback.value = config.cooperation_feedback
homeEntriesEnabled.value = config.home_entries_enabled
publishOptions.value = config.publish_options
homeCache.saveHomeConfig({
announcements: config.announcements,
bannerSlides: config.banners,
awRecycle: config.aw_recycle,
cooperationFeedback: config.cooperation_feedback,
homeEntriesEnabled: config.home_entries_enabled,
publishOptions: config.publish_options,
})
} catch {
@@ -490,6 +513,7 @@ async function loadHomeConfig() {
bannerSlides.value = defaultHomeBanners
awRecycle.value = { ...defaultAWRecycleConfig }
cooperationFeedback.value = { ...defaultCooperationFeedbackConfig }
homeEntriesEnabled.value = defaultHomeEntriesEnabled
publishOptions.value = emptyListingPublishOptions
}
}
@@ -556,10 +580,14 @@ async function onRefresh() {
bannerSlides.value = nextHomeConfig.banners
awRecycle.value = nextHomeConfig.aw_recycle
cooperationFeedback.value = nextHomeConfig.cooperation_feedback
homeEntriesEnabled.value = nextHomeConfig.home_entries_enabled
publishOptions.value = nextHomeConfig.publish_options
homeCache.saveHomeConfig({
announcements: nextHomeConfig.announcements,
bannerSlides: nextHomeConfig.banners,
awRecycle: nextHomeConfig.aw_recycle,
cooperationFeedback: nextHomeConfig.cooperation_feedback,
homeEntriesEnabled: nextHomeConfig.home_entries_enabled,
publishOptions: nextHomeConfig.publish_options,
})
showToast({ message: '刷新成功', icon: 'passed' })
@@ -944,14 +972,22 @@ if (!restoreHomeConfigFromCache()) {
</van-swipe-item>
</van-swipe>
<div class="biz-entry-grid" aria-label="业务入口">
<RouterLink v-if="!siteStore.isDistribution" class="biz-entry" to="/crash">
<div
v-if="showBizEntryGrid"
class="biz-entry-grid"
:class="{
'single-entry': bizEntryCount === 1,
'two-entries': bizEntryCount === 2,
}"
aria-label="业务入口"
>
<RouterLink v-if="showCrashEntry" class="biz-entry" to="/crash">
<strong>撞车</strong>
<small>选购商品 · 一键找客服</small>
<em class="biz-entry-badge">NEW</em>
</RouterLink>
<button
v-if="awRecycle.enabled"
v-if="showAWRecycle"
type="button"
class="biz-entry aw-recycle"
@click="openAWRecycle"
@@ -960,7 +996,7 @@ if (!restoreHomeConfigFromCache()) {
<small>{{ awRecycle.subtitle }}</small>
</button>
<button
v-if="cooperationFeedback.enabled"
v-if="showCooperationFeedback"
type="button"
class="biz-entry cooperation-feedback"
@click="openCooperationFeedback"
+14
View File
@@ -1,8 +1,13 @@
import { defineStore } from 'pinia'
import {
defaultAWRecycleConfig,
defaultCooperationFeedbackConfig,
defaultHomeAnnouncements,
defaultHomeBanners,
defaultHomeEntriesEnabled,
type AWRecycleConfig,
type CooperationFeedbackConfig,
type HomeBannerSlide,
} from '@/features/listings/api/homeConfig'
import {
@@ -44,6 +49,9 @@ export const useMobileHomeCacheStore = defineStore('mobileHomeCache', {
scrollTop: 0,
announcements: [...defaultHomeAnnouncements] as string[],
bannerSlides: defaultHomeBanners.map(slide => ({ ...slide })) as HomeBannerSlide[],
awRecycle: { ...defaultAWRecycleConfig } as AWRecycleConfig,
cooperationFeedback: { ...defaultCooperationFeedbackConfig } as CooperationFeedbackConfig,
homeEntriesEnabled: defaultHomeEntriesEnabled,
publishOptions: { ...emptyListingPublishOptions } as ListingPublishOptions,
homeConfigLoaded: false,
}),
@@ -77,10 +85,16 @@ export const useMobileHomeCacheStore = defineStore('mobileHomeCache', {
saveHomeConfig(payload: {
announcements: string[]
bannerSlides: HomeBannerSlide[]
awRecycle: AWRecycleConfig
cooperationFeedback: CooperationFeedbackConfig
homeEntriesEnabled: boolean
publishOptions: ListingPublishOptions
}) {
this.announcements = payload.announcements
this.bannerSlides = payload.bannerSlides
this.awRecycle = payload.awRecycle
this.cooperationFeedback = payload.cooperationFeedback
this.homeEntriesEnabled = payload.homeEntriesEnabled
this.publishOptions = payload.publishOptions
this.homeConfigLoaded = true
},
+4
View File
@@ -5,6 +5,7 @@ import {
defaultCooperationFeedbackConfig,
defaultHomeAnnouncements,
defaultHomeBanners,
defaultHomeEntriesEnabled,
type AWRecycleConfig,
type CooperationFeedbackConfig,
type HomeBannerSlide,
@@ -30,6 +31,7 @@ export interface PcHomeConfigSnapshot {
awRecycle: AWRecycleConfig
cooperationFeedback: CooperationFeedbackConfig
publishOptions: ListingPublishOptions
homeEntriesEnabled: boolean
}
/** PC端首页列表/配置/滚动缓存:与页面实例解耦,从详情页返回时可无缝秒级恢复,避免回顶闪烁 */
@@ -47,6 +49,7 @@ export const usePcHomeCacheStore = defineStore('pcHomeCache', {
awRecycle: { ...defaultAWRecycleConfig } as AWRecycleConfig,
cooperationFeedback: { ...defaultCooperationFeedbackConfig } as CooperationFeedbackConfig,
publishOptions: { ...emptyListingPublishOptions } as ListingPublishOptions,
homeEntriesEnabled: defaultHomeEntriesEnabled,
homeConfigLoaded: false,
}),
getters: {
@@ -77,6 +80,7 @@ export const usePcHomeCacheStore = defineStore('pcHomeCache', {
this.awRecycle = payload.awRecycle
this.cooperationFeedback = payload.cooperationFeedback
this.publishOptions = payload.publishOptions
this.homeEntriesEnabled = payload.homeEntriesEnabled
this.homeConfigLoaded = true
},
clearList() {